]> gitweb.michael.orlitzky.com - dead/htsn.git/blobdiff - src/Main.hs
Add a bunch of new options allowing htsn to daemonize.
[dead/htsn.git] / src / Main.hs
index 6b18c8646d311bdcaef94042561299f1a5836241..b6403614d5d46af193e1f7b2b2adef5ccc8c4bcd 100644 (file)
@@ -4,19 +4,19 @@
 module Main
 where
 
-import Control.Concurrent (threadDelay)
-import Control.Exception.Base (bracket)
-import Control.Monad (when)
-import Data.List (isPrefixOf)
-import Data.Maybe (isNothing)
-import Data.Monoid ((<>))
+import Control.Concurrent ( threadDelay )
+import Control.Exception.Base ( bracket )
+import Control.Monad ( when )
+import Data.List ( isPrefixOf )
+import Data.Maybe ( isNothing )
+import Data.Monoid ( (<>) )
 import Network (
   connectTo,
   PortID (PortNumber) )
-import System.Console.CmdArgs (def)
-import System.Directory (doesFileExist)
-import System.Exit (ExitCode(..), exitWith)
-import System.FilePath ((</>))
+import System.Console.CmdArgs ( def )
+import System.Directory ( doesFileExist )
+import System.Exit ( ExitCode(..), exitWith )
+import System.FilePath ( (</>) )
 import System.IO (
   BufferMode (NoBuffering),
   Handle,
@@ -27,15 +27,16 @@ import System.IO (
   hSetBuffering,
   stderr,
   stdout )
-import System.IO.Error (catchIOError)
-import System.Timeout (timeout)
+import System.IO.Error ( catchIOError )
+import System.Timeout ( timeout )
 
-import CommandLine (get_args)
-import Configuration (Configuration(..), merge_optional)
+import CommandLine ( get_args )
+import Configuration ( Configuration(..), merge_optional )
 import ExitCodes (
   exit_no_feed_hosts,
   exit_no_password,
-  exit_no_username )
+  exit_no_username,
+  exit_pidfile_exists )
 import Logging (
   init_logging,
   log_debug,
@@ -51,33 +52,40 @@ import Terminal (
   display_info,
   display_sent,
   display_warning )
-import TSN.FeedHosts (FeedHosts(..))
-import TSN.Xml (parse_xmlfid, xml_prologue)
+import TSN.FeedHosts ( FeedHosts(..) )
+import TSN.Xml ( parse_xmlfid )
+import Unix ( full_daemonize )
 
-
--- | Warning! This does not automatically append a newline. The output
--- is displayed/logged as-is, for, you know, debug purposes.
+-- | Display and log debug information. WARNING! This does not
+--   automatically append a newline. The output is displayed/logged
+--   as-is, for, you know, debug purposes.
 report_debug :: String -> IO ()
 report_debug s = do
   display_debug s
   log_debug s
 
+
+-- | Display and log an error condition. This will prefix the error
+--   with "ERROR: " when displaying (but not logging) it so that it
+--   stands out.
+--
 report_error :: String -> IO ()
 report_error s = do
   display_error $ "ERROR: " ++ s
   log_error s
 
+
+-- | Display and log an informational (status) message.
 report_info :: String -> IO ()
 report_info s = do
   display_info s
   log_info s
 
--- | Warning! This does not automatically append a newline.
-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.
+--
 report_warning :: String -> IO ()
 report_warning s = do
   display_warning $ "WARNING: " ++ s
@@ -103,10 +111,9 @@ recv_line h = do
 --
 save_document :: Configuration -> String -> IO ()
 save_document cfg doc =
-  case maybe_path of
-    Nothing ->
-      report_error "Document missing XML_File_ID element."
-    Just path -> do
+  case either_path of
+    Left err -> report_error err
+    Right path -> do
       already_exists <- doesFileExist path
       when already_exists $ do
         let msg = "File " ++ path ++ " already exists, overwriting."
@@ -114,31 +121,42 @@ save_document cfg doc =
       writeFile path doc
       report_info $ "Wrote file: " ++ path ++ "."
   where
+    -- All the fmaps are because we're working inside a Maybe.
     xmlfid = fmap show (parse_xmlfid doc)
     filename = fmap (++ ".xml") xmlfid
-    maybe_path = fmap ((output_directory cfg) </>) filename
+    either_path = fmap ((output_directory cfg) </>) filename
 
 
--- | Loop forever, writing the buffer to file whenever a new XML
---   prologue is seen. This is the low-level "loop forever" function
---   that we stay in as long as we are connected to one feed.
+-- | Loop forever, writing the buffer to file whenever a </message>
+--   tag is seen. This is the low-level "loop forever" function that
+--   we stay in as long as we are connected to one feed.
+--
+--   The documentation at
+--   <http://www.sportsnetworkdata.com/feeds/xml-levels.asp> states
+--   that \<message\> will always be the root element of the XML
+--   documents, and \</message\> will be the final line transmitted
+--   for a given document. We therefore rely on this to simplify
+--   processing.
 --
 loop :: Configuration -> Handle -> [String] -> IO ()
 loop !cfg !h !buffer = do
   line <- recv_line h
+  let new_buffer = line : buffer
 
-  if (xml_prologue `isPrefixOf` line && not (null buffer))
+  -- Use isPrefixOf to avoid line-ending issues. Hopefully they won't
+  -- send invalid junk (on the same line) after closing the root
+  -- element.
+  if "</message>" `isPrefixOf` line
   then do
-    -- This is the beginning of a new document, and we have an "old"
-    -- one to save. The buffer is in reverse (newest first) order,
-    -- though, so we have to reverse it first. We then concatenate all
-    -- of its lines into one big string.
-    let document = concat $ reverse buffer
+    -- The buffer is in reverse (newest first) order, though, so we
+    -- have to reverse it first. We then concatenate all of its lines
+    -- into one big string.
+    let document = concat $ reverse new_buffer
     save_document cfg document
-    loop cfg h [line] -- Empty the buffer before looping again.
+    loop cfg h [] -- Empty the buffer before looping again.
   else
     -- Append line to the head of the buffer and loop.
-    loop cfg h (line : buffer)
+    loop cfg h new_buffer
 
 
 log_in :: Configuration -> Handle -> IO ()
@@ -165,6 +183,7 @@ log_in cfg h = do
     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
@@ -179,7 +198,7 @@ log_in cfg h = do
 
 connect_and_loop :: Configuration -> String -> IO ()
 connect_and_loop cfg host = do
-  report_info $ "Connecting to " ++ host ++ "..."
+  report_info $ "Connecting to " ++ host ++ "."
   bracket acquire_handle release_handle action
   return ()
   where
@@ -198,8 +217,14 @@ connect_and_loop cfg host = do
       --
       -- If we dump the packets with tcpdump, it looks like their
       -- software is getting confused: they send us some XML in
-      -- the middle of the log-in procedure. In any case, the easiest
-      -- fix is to disconnect and try again.
+      -- the middle of the log-in procedure.
+      --
+      -- On the other hand, the documentation at
+      -- <http://www.sportsnetworkdata.com/feeds/xml-levels.asp>
+      -- states that you can only make one connection per username to
+      -- a given host. So maybe they're simply rejecting the username
+      -- in an unfriendly fashion. In any case, the easiest fix is to
+      -- disconnect and try again.
       --
       login_worked <- timeout five_seconds $ log_in cfg h
       case login_worked of
@@ -231,7 +256,7 @@ main = do
   -- logging before the missing parameter checks below so that we can
   -- log the errors.
   let cfg = (def :: Configuration) `merge_optional` opt_config
-  init_logging (syslog cfg)
+  init_logging (log_file cfg) (log_level cfg) (syslog cfg)
 
   -- Check the optional config for missing required options. This is
   -- necessary because if the user specifies an empty list of
@@ -250,13 +275,27 @@ main = do
     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
@@ -270,5 +309,5 @@ main = do
       let hosts = get_feed_hosts $ feed_hosts cfg
       let host = hosts !! feed_host_idx
       catchIOError (connect_and_loop cfg host) (report_error . show)
-      thread_sleep 10 -- Wait 10s before attempting to reconnect.
+      thread_sleep 5 -- Wait 5s before attempting to reconnect.
       round_robin cfg $ (feed_host_idx + 1) `mod` (length hosts)