# needs to go in %APPDATA%, or C:\Users\<username>\Application Data.
-# The username used to connect to the feed.
-#
-# Default: none (required)
-#
-# username = "whoever"
-
-
-# The password associated with your TSN username.
-#
-# Default: none (required)
+# Run in the background as a daemon?
#
-# password = "whatever"
-
-
-# By default, XML files will be written to the current working
-# directory. Often this is not desirable, and you would rather save
-# them to a specific location. Specify it here.
-#
-# Default: "."
+# Default: false
#
-# output-directory = "/var/lib/htsn"
+# daemonize = True
# A list of hostnames that supply the feed. You probably don't need to
# feed-hosts = [ "hostname1", "hostname2", ... ]
-# Do you want to log to syslog? On Windows this will attempt to
-# communicate (over UDP) with a syslog daemon on localhost, which will
-# most likely not work.
-#
-# Default: False
-#
-# syslog = True
-
-
# If you specify a file path here, logs will be written to it
# (possibly in addition to syslog). Can be either a relative or
# absolute path. It will not be auto-rotated; use something like
# Default: "INFO"
#
# log_level = "WARNING"
+
+
+# By default, XML files will be written to the current working
+# directory. Often this is not desirable, and you would rather save
+# them to a specific location. Specify it here.
+#
+# Default: "."
+#
+# output-directory = "/var/lib/htsn"
+
+
+# The password associated with your TSN username.
+#
+# Default: none (required)
+#
+# password = "whatever"
+
+
+# (Daemon mode only) Create a PID file in the given location.
+#
+# Default: /run/htsn.pid
+#
+# pidfile = /var/run/htsn.pid
+
+
+# (Daemon mode only) Run htsn as the specified system grup.
+#
+# Default: the current group
+#
+# run-as-group = htsn
+
+
+# (Daemon mode only) Run htsn as the specified system user.
+#
+# Default: the current user
+#
+# run-as-user = htsn
+
+# Do you want to log to syslog? On Windows this will attempt to
+# communicate (over UDP) with a syslog daemon on localhost, which will
+# most likely not work.
+#
+# Default: False
+#
+# syslog = True
+
+
+# The username used to connect to the feed.
+#
+# Default: none (required)
+#
+# username = "whoever"
configurator == 0.2.*,
directory == 1.2.*,
filepath == 1.3.*,
+ hdaemonize == 0.4.*,
hslogger == 1.2.*,
hxt == 9.3.*,
MissingH == 1.2.*,
configurator == 0.2.*,
directory == 1.2.*,
filepath == 1.3.*,
+ hdaemonize == 0.4.*,
hslogger == 1.2.*,
hxt == 9.3.*,
MissingH == 1.2.*,
my_summary = program_name ++ "-" ++ (showVersion version)
+daemonize_help :: String
+daemonize_help =
+ "Run as a daemon, in the background."
+
-- | A description of the "log_file" option.
log_file_help :: String
log_file_help =
log_level_help =
"How verbose should the logs be? One of INFO, WARNING, ERROR."
+-- | A description of the "output_directory" option.
+output_directory_help :: String
+output_directory_help =
+ "Directory in which to output the XML files; must be writable"
+
-- | A description of the "password" option.
password_help :: String
password_help =
"Password to use when connecting to the feed"
--- | A description of the "output_directory" option.
-output_directory_help :: String
-output_directory_help =
- "Directory in which to output the XML files; must be writable"
+pidfile_help :: String
+pidfile_help =
+ "Location to create PID file (daemon only)."
+
+run_as_group_help :: String
+run_as_group_help =
+ "System group to run as (daemon only)."
+
+run_as_user_help :: String
+run_as_user_help =
+ "System user to run under (daemon only)."
-- | A description of the "syslog" option.
syslog_help :: String
-- Use an empty list for feed_hosts since cmdargs will appends to
-- the default when the user supplies feed hosts. If he specifies
-- any, those are all we should use.
+ daemonize = def &= typ "BOOL" &= help daemonize_help,
feed_hosts = def &= typ "HOSTNAMES" &= args,
- log_file = def &= typFile &= help log_file_help,
- log_level = def &= typ "LEVEL" &= help log_level_help,
- password = def &= typ "PASSWORD" &= help password_help,
- output_directory = def &= typDir &= help output_directory_help,
- syslog = def &= typ "BOOL" &= help syslog_help,
- username = def &= typ "USERNAME" &= help username_help }
+ log_file = def &= typFile &= help log_file_help,
+ log_level = def &= typ "LEVEL" &= help log_level_help,
+ output_directory = def &= typDir &= help output_directory_help,
+ password = def &= typ "PASSWORD" &= help password_help,
+ pidfile = def &= typFile &= help pidfile_help,
+ run_as_group = def &= typ "GROUP" &= help run_as_group_help,
+ run_as_user = def &= typ "USER" &= help run_as_user_help,
+ syslog = def &= typ "BOOL" &= help syslog_help,
+ username = def &= typ "USERNAME" &= help username_help }
&= program program_name
&= summary my_summary
&= details [description]
data Configuration =
Configuration {
+ daemonize :: Bool,
feed_hosts :: FeedHosts,
log_file :: Maybe FilePath,
log_level :: Priority,
- password :: String,
output_directory :: FilePath,
+ password :: String,
+ pidfile :: FilePath,
+ run_as_group :: Maybe String,
+ run_as_user :: Maybe String,
syslog :: Bool,
username :: String }
deriving (Show)
-- | A Configuration with all of its fields set to their default
-- values.
instance Default Configuration where
- def = Configuration def def INFO def "." def def
+ def = Configuration {
+ daemonize = def,
+ feed_hosts = def,
+ log_file = def,
+ log_level = INFO,
+ output_directory = ".",
+ password = def,
+ pidfile = "/run/htsn.pid",
+ run_as_group = def,
+ run_as_user = def,
+ syslog = def,
+ username = def }
-- | Merge a Configuration with an OptionalConfiguration. This is more
-> Configuration
merge_optional cfg opt_cfg =
Configuration
+ (merge (daemonize cfg) (OC.daemonize opt_cfg))
all_feed_hosts
(OC.merge_maybes (log_file cfg) (OC.log_file opt_cfg))
(merge (log_level cfg) (OC.log_level opt_cfg))
- (merge (password cfg) (OC.password opt_cfg))
(merge (output_directory cfg) (OC.output_directory opt_cfg))
+ (merge (password cfg) (OC.password opt_cfg))
+ (merge (pidfile cfg) (OC.pidfile opt_cfg))
+ (OC.merge_maybes (run_as_group cfg) (OC.run_as_group opt_cfg))
+ (OC.merge_maybes (run_as_user cfg) (OC.run_as_user opt_cfg))
(merge (syslog cfg) (OC.syslog opt_cfg))
(merge (username cfg) (OC.username opt_cfg))
where
module ExitCodes (
exit_no_feed_hosts,
exit_no_password,
- exit_no_username )
+ exit_no_username,
+ exit_pidfile_exists )
where
-- | No feed hosts were given on the command line or in the config file.
-- | No username was given on the command line or in the config file.
exit_no_username :: Int
exit_no_username = 3
+
+-- | When running as a daemon, the existence of a fixed PID file is
+-- used to determine whether or not the daemon is already
+-- running. If the PID file already exists, we shouldn't start.
+exit_pidfile_exists :: Int
+exit_pidfile_exists = 4
import ExitCodes (
exit_no_feed_hosts,
exit_no_password,
- exit_no_username )
+ exit_no_username,
+ exit_pidfile_exists )
import Logging (
init_logging,
log_debug,
display_warning )
import TSN.FeedHosts ( FeedHosts(..) )
import TSN.Xml ( parse_xmlfid )
-
+import Unix ( full_daemonize )
-- | Display and log debug information. WARNING! This does not
-- automatically append a newline. The output is displayed/logged
log_info s
--- | A special case of report_debug for reporting the two bits of data
--- that we sent to TSN: the username and password.
---
-report_sent :: String -> IO ()
-report_sent s = do
- display_sent s
- log_debug s
-
-
-- | Display and log a warning. This will prefix the warning with
-- "WARNING: " when displaying (but not logging) it so that it
-- stands out.
send_line h' s = do
let line = s ++ "\r\n"
hPutStr h' line
+ -- Don't log the username/password!
display_sent line
recv_chars :: Int -> Handle -> IO String
report_error "No username supplied."
exitWith (ExitFailure exit_no_username)
+ when (daemonize cfg) $ do
+ pidfile_exists <- doesFileExist (pidfile cfg)
+ when pidfile_exists $ do
+ report_error $ "PID file " ++ (pidfile cfg) ++ " already exists. "
+ ++ "Refusing to start."
+ exitWith (ExitFailure exit_pidfile_exists)
+
-- This may be superstition (and I believe stderr is unbuffered),
-- but it can't hurt.
hSetBuffering stderr NoBuffering
hSetBuffering stdout NoBuffering
- -- Begin connecting to our feed hosts, starting with the first one.
- round_robin cfg 0
+ -- The rest of the program is kicked off by the following line which
+ -- begins connecting to our feed hosts, starting with the first one,
+ -- and proceeds in a round-robin fashion.
+ let run_program = round_robin cfg 0
+
+ -- If we were asked to daemonize, do that; otherwise just run the thing.
+ if (daemonize cfg)
+ then full_daemonize cfg run_program
+ else run_program
where
-- | This is the top-level "loop forever" function. If an
--
data OptionalConfiguration =
OptionalConfiguration {
+ daemonize :: Maybe Bool,
feed_hosts :: FeedHosts,
log_file :: Maybe FilePath,
log_level :: Maybe Priority,
- password :: Maybe String,
output_directory :: Maybe FilePath,
+ password :: Maybe String,
+ pidfile :: Maybe FilePath,
+ run_as_group :: Maybe String,
+ run_as_user :: Maybe String,
syslog :: Maybe Bool,
username :: Maybe String }
deriving (Show, Data, Typeable)
instance Monoid OptionalConfiguration where
-- | An empty OptionalConfiguration.
mempty = OptionalConfiguration
+ Nothing
(FeedHosts [])
Nothing
Nothing
Nothing
Nothing
Nothing
+ Nothing
+ Nothing
+ Nothing
-- | Combine @cfg1@ and @cfg2@, giving precedence to @cfg2@.
cfg1 `mappend` cfg2 =
OptionalConfiguration
+ (merge_maybes (daemonize cfg1) (daemonize cfg2))
all_feed_hosts
(merge_maybes (log_file cfg1) (log_file cfg2))
(merge_maybes (log_level cfg1) (log_level cfg2))
- (merge_maybes (password cfg1) (password cfg2))
(merge_maybes (output_directory cfg1) (output_directory cfg2))
+ (merge_maybes (password cfg1) (password cfg2))
+ (merge_maybes (pidfile cfg1) (pidfile cfg2))
+ (merge_maybes (run_as_group cfg1) (run_as_group cfg2))
+ (merge_maybes (run_as_user cfg1) (run_as_user cfg2))
(merge_maybes (syslog cfg1) (syslog cfg2))
(merge_maybes (username cfg1) (username cfg2))
where
return "$(HOME)")
let user_config_path = home </> ".htsnrc"
cfg <- DC.load [ DC.Optional user_config_path ]
+ cfg_daemonize <- DC.lookup cfg "daemonize"
+ cfg_feed_hosts <- DC.lookup cfg "feed_hosts"
cfg_log_file <- DC.lookup cfg "log_file"
cfg_log_level <- DC.lookup cfg "log_level"
- cfg_password <- DC.lookup cfg "password"
cfg_output_directory <- DC.lookup cfg "output_directory"
+ cfg_password <- DC.lookup cfg "password"
+ cfg_pidfile <- DC.lookup cfg "pidfile"
+ cfg_run_as_group <- DC.lookup cfg "run_as_group"
+ cfg_run_as_user <- DC.lookup cfg "run_as_user"
cfg_syslog <- DC.lookup cfg "syslog"
cfg_username <- DC.lookup cfg "username"
- cfg_feed_hosts <- DC.lookup cfg "feed_hosts"
return $ OptionalConfiguration
+ cfg_daemonize
(fromMaybe (FeedHosts []) cfg_feed_hosts)
cfg_log_file
cfg_log_level
- cfg_password
cfg_output_directory
+ cfg_password
+ cfg_pidfile
+ cfg_run_as_group
+ cfg_run_as_user
cfg_syslog
cfg_username
-
--- /dev/null
+module Unix
+where
+
+import Control.Concurrent ( ThreadId, myThreadId )
+import Control.Exception ( throwTo )
+import System.Exit ( ExitCode( ExitSuccess ) )
+import System.Posix (
+ GroupEntry ( groupID ),
+ GroupID,
+ Handler ( Catch ),
+ UserEntry ( userID ),
+ UserID,
+ getGroupEntryForName,
+ getProcessID,
+ getRealGroupID,
+ getRealUserID,
+ getUserEntryForName,
+ installHandler,
+ removeLink,
+ setGroupID,
+ setUserID,
+ sigTERM )
+import System.Posix.Daemonize ( daemonize )
+
+import Configuration (
+ Configuration( pidfile,
+ run_as_group,
+ run_as_user ))
+import Logging ( log_info )
+
+get_user_id :: Maybe String -> IO UserID
+get_user_id Nothing = getRealUserID
+get_user_id (Just s) = fmap userID (getUserEntryForName s)
+
+get_group_id :: Maybe String -> IO GroupID
+get_group_id Nothing = getRealGroupID
+get_group_id (Just s) = fmap groupID (getGroupEntryForName s)
+
+graceful_shutdown :: Configuration -> ThreadId -> IO ()
+graceful_shutdown cfg main_thread_id = do
+ log_info "SIGTERM received, removing PID file and shutting down."
+ removeLink (pidfile cfg)
+ throwTo main_thread_id ExitSuccess
+
+full_daemonize :: Configuration -> IO () -> IO ()
+full_daemonize cfg program = do
+ -- This is the 'daemonize' from System.Posix.Daemonize.
+ daemonize program'
+ where
+ -- We need to do all this stuff *after* we daemonize.
+ program' = do
+ -- First write the PID file which probably requires root.
+ pid <- getProcessID
+ writeFile (pidfile cfg) (show pid)
+
+ -- We need to pass the thread ID to the signal handler so it
+ -- knows which process to "exit."
+ tid <- myThreadId
+ _ <- installHandler sigTERM (Catch (graceful_shutdown cfg tid)) Nothing
+
+ -- Then drop privileges.
+ get_user_id (run_as_user cfg) >>= setUserID
+ get_group_id (run_as_group cfg) >>= setGroupID
+
+ -- Finally run the program we were asked to.
+ program