{-|
Module      : System.Systemd.Daemon.Fd
Description : File descriptor based socket activation/management
              using systemd
Copyright   : (c) Romain Gérard, 2014
                  David Fisher, 2013
                  Lukas Epple, 2019
License     : BSD3
Maintainer  : romain.gerard@erebe.eu
Stability   : stable
Portability : Requires Systemd or will fail otherwise

This module implements all functions from "System.Systemd.Daemon"
that require or return 'Network.Socket.Socket's purely using 'Fd's.
This is especially useful if you have to do low level IO using
file descriptors or use a different socket library than @network@.

The API is exactly the same as "System.Systemd.Daemon" except that
'Network.Socket.Socket's have been replaced by 'Fd's (actually
"System.Systemd.Daemon" uses this module internally). This also means
that "System.Systemd.Daemon.Fd" and "System.Systemd.Daemon" expose
conflicting functions. You either have to import "System.Systemd.Daemon.Fd"
@qualified@ or like so:

@
import System.Systemd.Daemon hiding ( notifyWithFD, storeFd
                                    , storeFdWithName
                                    , getActivatedSockets
                                    , getActivatedSocketsWithNames )
import System.Systemd.Daemon.Fd
@

The functions in "System.Systemd.Daemon" that are not implemented
in this module are 100% compatible with "System.Systemd.Daemon.Fd".
-}
module System.Systemd.Daemon.Fd
  ( -- * Notify functions
    notifyWithFD
  , storeFd
  , storeFdWithName
    -- * Socket activation functions
  , getActivatedSockets
  , getActivatedSocketsWithNames
  ) where

import           Control.Monad
import           Control.Monad.IO.Class    (liftIO)
import           Control.Monad.Trans.Maybe
import qualified Data.ByteString.Char8     as BC
import           Foreign.C.Types           (CInt (..))
import           Network.Socket            (setNonBlockIfNeeded)
import           System.Posix.Env          (getEnv)
import           System.Posix.Process
import           System.Posix.Types        (Fd (..))
import           System.Systemd.Internal

fdStart :: CInt
fdStart = 3

-- | Notify Systemd to store a file descriptor for us. This together
--   with 'getActivatedSockets' allows for zero downtime
--   restarts and socket activation.
--
--   Equivalent to standard 'System.Systemd.Daemon.storeFd'
storeFd :: Fd -> IO (Maybe ())
storeFd = notifyWithFD False "FDSTORE=1"

-- | Like 'storeFd', but associate the file descriptor with a name.
--   Best used along with 'getActivatedSocketsWithNames'.
--
--   Equivalent to standard 'System.Systemd.Daemon.storeFdWithName'
storeFdWithName :: Fd -> String -> IO (Maybe ())
storeFdWithName fd name = notifyWithFD False ("FDSTORE=1\nFDNAME=" ++ name) fd

-- | Same as 'System.Systemd.Daemon.notify', but send along a 'Fd'.
--   Note that the caller must set the message, i. e. send @FDSTORE=1@
--   to actually store the file descriptor. In most cases it is probably best
--   to use 'storeFd' or the notify-functions from "System.Systemd.Daemon".
--
--   Equivalent to standard 'System.Systemd.Daemon.notifyWithFD'.
notifyWithFD :: Bool -> String -> Fd -> IO (Maybe ())
notifyWithFD unset_env state sock = notifyWithFD_ unset_env state (Just sock)

-- | Return 'Just' a list of file descriptors if the current process
--   has been activated with one or more socket by systemd, 'Nothing'
--   otherwise.
--
--   The file descriptors are in the same order as the sockets in the
--   associated @.socket@ file. The sockets will have their family, type,
--   and status set according to the @.socket@ file.
--
--   Equivalent to standard 'System.Systemd.Daemon.getActivatedSockets'
getActivatedSockets :: IO (Maybe [Fd])
getActivatedSockets = runMaybeT $ do
    listenPid     <- read <$> MaybeT (getEnv "LISTEN_PID")
    listenFDs     <- read <$> MaybeT (getEnv "LISTEN_FDS")

    myPid <- liftIO getProcessID
    guard $ listenPid == myPid

    mapM (\fd -> liftIO (setNonBlockIfNeeded fd) >> pure (Fd fd))
         [fdStart .. fdStart + listenFDs - 1]

-- | Like 'getActivatedSockets', but also return the associated names.
--   If a file descriptor has no associated name, it will be a generic
--   one set by systemd.
--
--   Equivalent to standard 'System.Systemd.Daemon.getActivatedSocketsWithNames'
getActivatedSocketsWithNames :: IO (Maybe [(Fd, String)])
getActivatedSocketsWithNames = runMaybeT $ do
    listenFDNames <- MaybeT (getEnv "LISTEN_FDNAMES")
    let listenFDNames' = fmap BC.unpack $ BC.split ':' $ BC.pack listenFDNames

    nonBlockFds <- MaybeT getActivatedSockets
    guard $ length nonBlockFds == length listenFDNames'

    return $ zip nonBlockFds listenFDNames'