{- Running a long or expensive batch operation niced.
 -
 - Copyright 2013 Joey Hess <id@joeyh.name>
 -
 - License: BSD-2-clause
 -}

{-# LANGUAGE CPP #-}

module Utility.Batch where

import Common

#if defined(linux_HOST_OS) || defined(__ANDROID__)
import Control.Concurrent.Async
import System.Posix.Process
#endif
import qualified Control.Exception as E

{- Runs an operation, at batch priority.
 -
 - This is done by running it in a bound thread, which on Linux can be set
 - to have a different nice level than the rest of the program. Note that
 - due to running in a bound thread, some operations may be more expensive
 - to perform. Also note that if the action calls forkIO or forkOS itself,
 - that will make a new thread that does not have the batch priority.
 -
 - POSIX threads do not support separate nice levels, so on other operating
 - systems, the action is simply ran.
 -}
batch :: IO a -> IO a
#if defined(linux_HOST_OS) || defined(__ANDROID__)
batch a = wait =<< batchthread
  where
	batchthread = asyncBound $ do
		setProcessPriority 0 maxNice
		a
#else
batch a = a
#endif

maxNice :: Int
maxNice = 19

{- Makes a command be run by whichever of nice, ionice, and nocache
 - are available in the path. -}
type BatchCommandMaker = (String, [CommandParam]) -> (String, [CommandParam])

getBatchCommandMaker :: IO BatchCommandMaker
getBatchCommandMaker = do
#ifndef mingw32_HOST_OS
	nicers <- filterM (inPath . fst)
		[ ("nice", [])
#ifndef __ANDROID__
		-- Android's ionice does not allow specifying a command,
		-- so don't use it.
		, ("ionice", ["-c3"])
#endif
		, ("nocache", [])
		]
	return $ \(command, params) ->
		case nicers of
			[] -> (command, params)
			(first:rest) -> (fst first, map Param (snd first ++ concatMap (\p -> fst p : snd p) rest ++ [command]) ++ params)
#else
	return id
#endif

toBatchCommand :: (String, [CommandParam]) -> IO (String, [CommandParam])
toBatchCommand v = do
	batchmaker <- getBatchCommandMaker
	return $ batchmaker v

{- Runs a command in a way that's suitable for batch jobs that can be
 - interrupted.
 -
 - If the calling thread receives an async exception, it sends the
 - command a SIGTERM, and after the command finishes shuttting down,
 - it re-raises the async exception. -}
batchCommand :: String -> [CommandParam] -> IO Bool
batchCommand command params = batchCommandEnv command params Nothing

batchCommandEnv :: String -> [CommandParam] -> Maybe [(String, String)] -> IO Bool
batchCommandEnv command params environ = do
	batchmaker <- getBatchCommandMaker
	let (command', params') = batchmaker (command, params)
	let p = proc command' $ toCommand params'
	(_, _, _, pid) <- createProcess $ p { env = environ }
	r <- E.try (waitForProcess pid) :: IO (Either E.SomeException ExitCode)
	case r of
		Right ExitSuccess -> return True
		Right _ -> return False
		Left asyncexception -> do
			terminateProcess pid
			void $ waitForProcess pid
			E.throwIO asyncexception
