{-# LANGUAGE CPP #-}
module System.Log.Handler.Syslog(
                                       SyslogHandler, 
                                       
                                       openlog,
                                       
#ifndef mingw32_HOST_OS
                                       openlog_local,
#endif
                                       openlog_remote,
                                       openlog_generic,
                                       
                                       Facility(..),
                                       Option(..)
                                       ) where
import qualified Control.Exception as E
import System.Log
import System.Log.Formatter
import System.Log.Handler
import Data.Bits
import qualified Network.Socket as S
import qualified Network.Socket.ByteString as SBS
import qualified Network.BSD as S
import Data.List (genericDrop)
#ifndef mingw32_HOST_OS
import System.Posix.Process(getProcessID)
#endif
import System.IO
import Control.Monad (void, when)
import UTF8
send :: S.Socket -> String -> IO Int
send s = SBS.send s . toUTF8BS
sendTo :: S.Socket -> String -> S.SockAddr -> IO Int
sendTo s str = SBS.sendTo s (toUTF8BS str)
code_of_pri :: Priority -> Int
code_of_pri p = case p of
                       EMERGENCY -> 0
                       ALERT -> 1
                       CRITICAL -> 2
                       ERROR -> 3
                       WARNING -> 4
                       NOTICE -> 5
                       INFO -> 6
                       DEBUG -> 7
data Facility =
              KERN                      
              | USER                    
              | MAIL                    
              | DAEMON                  
              | AUTH                    
              | SYSLOG                  
              | LPR                     
              | NEWS                    
              | UUCP                    
              | CRON                    
              | AUTHPRIV                
              | FTP                     
              | LOCAL0                  
              | LOCAL1
              | LOCAL2
              | LOCAL3
              | LOCAL4
              | LOCAL5
              | LOCAL6
              | LOCAL7
                deriving (Eq, Show, Read)
code_of_fac :: Facility -> Int
code_of_fac f = case f of
                       KERN -> 0
                       USER -> 1
                       MAIL -> 2
                       DAEMON -> 3
                       AUTH -> 4
                       SYSLOG -> 5
                       LPR -> 6
                       NEWS -> 7
                       UUCP -> 8
                       CRON -> 9
                       AUTHPRIV -> 10
                       FTP -> 11
                       LOCAL0 -> 16
                       LOCAL1 -> 17
                       LOCAL2 -> 18
                       LOCAL3 -> 19
                       LOCAL4 -> 20
                       LOCAL5 -> 21
                       LOCAL6 -> 22
                       LOCAL7 -> 23
makeCode :: Facility -> Priority -> Int
makeCode fac pri =
    let faccode = code_of_fac fac
        pricode = code_of_pri pri in
        (faccode `shiftL` 3) .|. pricode
data Option = PID                       
            | PERROR                    
            deriving (Eq,Show,Read)
data SyslogHandler = SyslogHandler {options :: [Option],
                                    facility :: Facility,
                                    identity :: String,
                                    logsocket :: S.Socket,
                                    address :: S.SockAddr,
                                    sock_type :: S.SocketType,
                                    priority :: Priority,
                                    formatter :: LogFormatter SyslogHandler
                                   }
openlog :: String                       
        -> [Option]                     
        -> Facility                     
        -> Priority                     
        -> IO SyslogHandler             
#ifdef mingw32_HOST_OS
openlog = openlog_remote S.AF_INET "localhost" 514
#elif darwin_HOST_OS
openlog = openlog_local "/var/run/syslog"
#else
openlog = openlog_local "/dev/log"
#endif
#ifndef mingw32_HOST_OS
openlog_local :: String                 
              -> String                 
              -> [Option]               
              -> Facility               
              -> Priority               
              -> IO SyslogHandler
openlog_local fifopath ident options' fac pri =
    do (s, t) <- do 
                    
                    
                    
                    
                    s <- S.socket S.AF_UNIX S.Stream 0
                    tryStream s `E.catch` (onIOException (fallbackToDgram s))
       openlog_generic s (S.SockAddrUnix fifopath) t ident options' fac pri
  where onIOException :: IO a -> E.IOException -> IO a
        onIOException a _ = a
        tryStream :: S.Socket -> IO (S.Socket, S.SocketType)
        tryStream s =
            do S.connect s (S.SockAddrUnix fifopath)
               return (s, S.Stream)
        fallbackToDgram :: S.Socket -> IO (S.Socket, S.SocketType)
        fallbackToDgram s =
            do S.close s 
               d <- S.socket S.AF_UNIX S.Datagram 0
               return (d, S.Datagram)
#endif
openlog_remote :: S.Family              
               -> S.HostName            
               -> S.PortNumber          
               -> String                
               -> [Option]              
               -> Facility              
               -> Priority              
               -> IO SyslogHandler
openlog_remote fam hostname port ident options' fac pri =
    do
    he <- S.getHostByName hostname
    s <- S.socket fam S.Datagram 0
    let addr = S.SockAddrInet port (head (S.hostAddresses he))
    openlog_generic s addr S.Datagram ident options' fac pri
openlog_generic :: S.Socket             
                -> S.SockAddr           
                -> S.SocketType         
                -> String               
                -> [Option]             
                -> Facility             
                -> Priority             
                -> IO SyslogHandler
openlog_generic sock addr sock_t ident opt fac pri =
    return (SyslogHandler {options = opt,
                            facility = fac,
                            identity = ident,
                            logsocket = sock,
                            address = addr,
                            sock_type = sock_t,
                            priority = pri,
                            formatter = syslogFormatter
                          })
syslogFormatter :: LogFormatter SyslogHandler
syslogFormatter sh (p,msg) logname =
    let format = "[$loggername/$prio] $msg"
    in varFormatter [] format sh (p,msg) logname
instance LogHandler SyslogHandler where
    setLevel sh p = sh{priority = p}
    getLevel sh = priority sh
    setFormatter sh f = sh{formatter = f}
    getFormatter sh = formatter sh
    emit sh (prio, msg) _ = do
      when (elem PERROR (options sh)) (hPutStrLn stderr msg)
      pidPart <- getPidPart
      void $ sendstr (toSyslogFormat msg pidPart)
      where
        sendstr :: String -> IO String
        sendstr [] = return []
        sendstr omsg = do
          sent <- case sock_type sh of
                    S.Datagram -> sendTo (logsocket sh) omsg (address sh)
                    S.Stream   -> send   (logsocket sh) omsg
          sendstr (genericDrop sent omsg)
        toSyslogFormat msg' pidPart =
            "<" ++ code ++ ">" ++ identity' ++ pidPart ++ ": " ++ msg' ++ "\0"
        code = show $ makeCode (facility sh) prio
        identity' = identity sh
        getPidPart = if elem PID (options sh)
                     then getPid >>= \pid -> return ("[" ++ pid ++ "]")
                     else return ""
        getPid :: IO String
        getPid =
#ifndef mingw32_HOST_OS
          getProcessID >>= return . show
#else
          return "windows"
#endif
    close sh = S.close (logsocket sh)