    module Propellor.Property.Cron (
            Times(..),
            job,
            niceJob,
            jobDropped,
            Propellor.Property.Cron.runPropellor
    ) where

    import Propellor.Base
    import qualified Propellor.Property.File as File
    import qualified Propellor.Property.Apt as Apt
    import Propellor.Bootstrap

    import Data.Char

    -- | When to run a cron job.
    --
    -- The Daily, Monthly, and Weekly options allow the cron job to be run
    -- by anacron, which is useful for non-servers.
    data Times
            = Times String -- ^ formatted as in crontab(5)
            | Daily
            | Weekly
            | Monthly

    -- | Installs a cron job, that will run as a specified user in a particular
    -- directory. Note that the Desc must be unique, as it is used for the
    -- cron job filename.
    --
    -- Only one instance of the cron job is allowed to run at a time, no matter
    -- how long it runs. This is accomplished using flock locking of the cron
    -- job file.
    --
    -- The cron job's output will only be emailed if it exits nonzero.
    job :: Desc -> Times -> User -> FilePath -> String -> Property DebianLike
    job desc times (User u) cddir command = combineProperties ("cronned " ++ desc) $ props
            & Apt.serviceInstalledRunning "cron"
            & Apt.installed ["util-linux", "moreutils"]
            & cronjobfile desc times `File.hasContent`
                    [ case times of
                            Times _ -> ""
                            _ -> "#!/bin/sh\nset -e"
                    , "# Generated by propellor"
                    , ""
                    , "SHELL=/bin/sh"
                    , "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin"
                    , ""
                    , case times of
                            Times t -> t ++ "\t" ++ u ++ "\tchronic "
                                    ++ shellEscape (scriptfile desc)
                            _ -> case u of
                                    "root" -> "chronic " ++ shellEscape (scriptfile desc)
                                    _ -> "chronic su " ++ u ++ " -c "
                                            ++ shellEscape (scriptfile desc)
                    ]
            & case times of
                    Times _ -> doNothing
                    _ -> (cronjobfile desc times)
                            `File.mode` combineModes (readModes ++ executeModes)
            -- Use a separate script because it makes the cron job name
            -- prettier in emails, and also allows running the job manually.
            & scriptfile desc `File.hasContent`
                    [ "#!/bin/sh"
                    , "# Generated by propellor"
                    , "set -e"
                    , "flock -n " ++ shellEscape (cronjobfile desc times)
                            ++ " sh -c " ++ shellEscape cmdline
                    ]
            & scriptfile desc `File.mode` combineModes (readModes ++ executeModes)
      where
            cmdline = "cd " ++ cddir ++ " && ( " ++ command ++ " )"

    -- | Removes a cron job created by 'job' or 'niceJob', as identified by the
    -- 'Desc' passed to those properties when the cronjob was set up
    --
    -- Those properties are not revertable because simply removing a cronjob does
    -- not undo the changes it might have made to the system, i.e., 'jobDropped' is
    -- not in the general case a reversion of 'job' or 'niceJob'
    jobDropped :: Desc -> Times -> Property UnixLike
    jobDropped desc times = combineProperties ("uncronned " ++ desc) $ props
            & File.notPresent (scriptfile desc)
            & File.notPresent (cronjobfile desc times)

    -- | Installs a cron job, and runs it niced and ioniced.
    niceJob :: Desc -> Times -> User -> FilePath -> String -> Property DebianLike
    niceJob desc times user cddir command = job desc times user cddir
            ("nice ionice -c 3 sh -c " ++ shellEscape command)

    -- | Installs a cron job to run propellor.
    runPropellor :: Times -> RevertableProperty DebianLike UnixLike
    runPropellor times = cronned <!> uncronned
      where
            cronned = withOS "propellor cron job" $ \w o -> do
                    bootstrapper <- getBootstrapper
                    ensureProperty w $
                            niceJob "propellor" times (User "root") localdir
                                    (bootstrapPropellorCommand bootstrapper o ++ "; ./propellor")
            uncronned = jobDropped "propellor" times

    -- Utility functions

    cronjobname :: Desc -> String
    cronjobname d = map sanitize d
      where
            sanitize c
                    | isAlphaNum c = c
                    | otherwise = '_'

    scriptfile :: Desc -> FilePath
    scriptfile d = "/usr/local/bin/" ++ (cronjobname d) ++ "_cronjob"

    cronjobfile :: Desc -> Times -> FilePath
    cronjobfile d times = "/etc" </> cronjobdir </> (cronjobname d)
      where
            cronjobdir = case times of
                    Times _ -> "cron.d"
                    Daily -> "cron.daily"
                    Weekly -> "cron.weekly"
                    Monthly -> "cron.monthly"