]> gitweb.michael.orlitzky.com - dead/halcyon.git/blob - src/Unix.hs
Add the Unix module and enable daemonization.
[dead/halcyon.git] / src / Unix.hs
1 -- | Non-portable code for daemonizing on unix.
2 --
3 module Unix
4 where
5
6 import Control.Concurrent ( ThreadId, myThreadId )
7 import Control.Exception ( throwTo )
8 import Control.Monad ( unless )
9 import System.Directory ( createDirectory, doesDirectoryExist )
10 import System.Exit ( ExitCode( ExitSuccess ) )
11 import System.FilePath ( dropFileName, dropTrailingPathSeparator )
12 import System.IO ( hPutStrLn, stderr )
13 import System.IO.Error ( catchIOError )
14 import System.Posix (
15 GroupEntry ( groupID ),
16 GroupID,
17 Handler ( Catch ),
18 UserEntry ( userID ),
19 UserID,
20 exitImmediately,
21 getGroupEntryForName,
22 getProcessID,
23 getRealGroupID,
24 getRealUserID,
25 getUserEntryForName,
26 installHandler,
27 removeLink,
28 setFileCreationMask,
29 setGroupID,
30 setOwnerAndGroup,
31 setUserID,
32 sigTERM )
33 import System.Posix.Daemonize ( daemonize )
34
35 import Configuration (
36 Cfg( pidfile,
37 run_as_group,
38 run_as_user ) )
39
40
41 -- | Retrieve the uid associated with the given system user name. We
42 -- take a Maybe String as an argument so the user name can be passed
43 -- in directly from the config.
44 --
45 get_user_id :: Maybe String -> IO UserID
46 get_user_id Nothing = getRealUserID
47 get_user_id (Just s) = fmap userID (getUserEntryForName s)
48
49
50 -- | Retrieve the gid associated with the given system group name. We
51 -- take a Maybe String as an argument so the group name can be
52 -- passed in directly from the config.
53 --
54 get_group_id :: Maybe String -> IO GroupID
55 get_group_id Nothing = getRealGroupID
56 get_group_id (Just s) = fmap groupID (getGroupEntryForName s)
57
58
59 -- | This function will be called in response to a SIGTERM; i.e. when
60 -- someone tries to kill our process. We simply delete the PID file
61 -- and signal our parent thread to quit (successfully).
62 --
63 -- If that doesn't work, report the error and quit rudely.
64 --
65 graceful_shutdown :: Cfg -> ThreadId -> IO ()
66 graceful_shutdown cfg main_thread_id = do
67 putStrLn "SIGTERM received, removing PID file and shutting down."
68 catchIOError try_nicely (\e -> do
69 hPutStrLn stderr ("ERROR: " ++ (show e))
70 exitImmediately ExitSuccess )
71 where
72 try_nicely = do
73 removeLink (pidfile cfg)
74 throwTo main_thread_id ExitSuccess
75
76
77 -- | Create the directory in which we intend to store the PID
78 -- file. This will *not* create any parent directories. The PID
79 -- directory will have its owner/group changed to the user/group
80 -- under which we'll be running. No permissions will be set; the
81 -- system's umask must allow owner-write.
82 --
83 -- This is intended to create one level beneath either /var/run or
84 -- /run which often do not survive a reboot.
85 --
86 -- If the directory already exists, it is left alone; that is, we
87 -- don't change its owner/group.
88 --
89 create_pid_directory :: FilePath -- ^ The directory to contain the PID file.
90 -> UserID -- ^ Owner of the new directory if created.
91 -> GroupID -- ^ Group of the new directory if created.
92 -> IO ()
93 create_pid_directory pid_directory uid gid = do
94 it_exists <- doesDirectoryExist pid_directory
95 unless it_exists $ do
96 putStrLn $ "Creating PID directory " ++ pid_directory ++ "."
97 createDirectory pid_directory
98 putStrLn $ "Changing owner/group of " ++ pid_directory ++
99 " to " ++ (show uid) ++ "/" ++ (show gid) ++ "."
100 setOwnerAndGroup pid_directory uid gid
101
102
103 -- | Write a PID file, install a SIGTERM handler, drop privileges, and
104 -- finally do the daemonization dance.
105 --
106 full_daemonize :: Cfg -> IO () -> IO ()
107 full_daemonize cfg program = do
108 uid <- get_user_id (run_as_user cfg)
109 gid <- get_group_id (run_as_group cfg)
110
111 -- This will have to be done as root and the result chowned to our
112 -- user/group, so it must happen before daemonizing.
113 let pid_directory = dropTrailingPathSeparator $ dropFileName $ pidfile cfg
114 create_pid_directory pid_directory uid gid
115
116 -- The call to 'daemonize' will set the umask to zero, but we want
117 -- to retain it. So, we set the umask to zero before 'daemonize'
118 -- can, so that we can record the previous umask value (returned by
119 -- setFileCreationMask).
120 orig_umask <- setFileCreationMask 0
121
122 -- This is the 'daemonize' from System.Posix.Daemonize.
123 daemonize (program' orig_umask uid gid)
124 where
125 -- We need to do all this stuff *after* we daemonize.
126 program' orig_umask uid gid = do
127 -- First we install a signal handler for sigTERM. We need to
128 -- pass the thread ID to the signal handler so it knows which
129 -- process to "exit."
130 tid <- myThreadId
131 _ <- installHandler sigTERM (Catch (graceful_shutdown cfg tid)) Nothing
132
133 -- Next we drop privileges. Group ID has to go first, otherwise
134 -- you ain't root to change groups.
135 setGroupID gid
136 setUserID uid
137
138 -- Now we create the PID file.
139 pid <- getProcessID
140
141 -- The PID file needs to be read-only for anyone but its
142 -- owner. Hopefully the umask accomplishes this!
143 _ <- setFileCreationMask orig_umask
144
145 -- When we later attempt to delete the PID file, it requires
146 -- write permission to the parent directory and not to the PID
147 -- file itself. Therefore, if that's going to work, this has to
148 -- work, even as a limited user.
149 writeFile (pidfile cfg) (show pid)
150
151 -- Finally run the program we were asked to.
152 program