{-# LANGUAGE PatternGuards, BangPatterns #-}

-- | Running system commands. On some platforms this may cause the command to be executed directly, so 
--   shell tricks won't work. The `Build` monad can be made to log commands executed with all versions
--   of `system` by setting `buildConfigLogSystem` in the `BuildConfig` passed to `runBuildPrintWithConfig`.
--
--   We define a lot of wrappers because executing system commands is the bread-and-butter of 
--   buildbots, and we usually need all the versions...

module BuildBox.Command.System 
	( module System.Exit

	-- * Wrappers
	, system
	, ssystem
	, qsystem
	, qssystem
	, ssystemOut
	, qssystemOut
	, systemTee
	, ssystemTee

	-- * The real function
	, systemTeeIO)
where
import BuildBox.Command.System.Internals
import BuildBox.Build
import Control.Concurrent
import System.Process	hiding (system)
import System.Exit
import System.IO
import Control.Monad

debug :: Bool
debug	= False

trace :: String -> IO ()
trace s	= when debug $ putStrLn s


-- Wrappers ---------------------------------------------------------------------------------------
-- | Run a system command, returning its exit code.
system :: String -> Build ExitCode
system cmd
 = do	(code, _, _)		<- systemTee True cmd ""
	return code


-- | Run a successful system command.
--   If the exit code is `ExitFailure` then throw an error in the `Build` monad.
ssystem :: String -> Build ()
ssystem cmd
 = do	(code, strOut, strErr)	<- systemTee True cmd ""

	when (code /= ExitSuccess)
	 $ throw $ ErrorSystemCmdFailed cmd code strOut strErr


-- | Quietly run a system command, returning its exit code.
qsystem :: String -> Build ExitCode
qsystem cmd
 = do	(code, _, _)		<- systemTee False cmd ""
	return code


-- | Quietly run a successful system command.
--   If the exit code is `ExitFailure` then throw an error in the `Build` monad.
qssystem :: String -> Build ()
qssystem cmd
 = do	(code, strOut, strErr)	<- systemTee False cmd ""

	when (code /= ExitSuccess)
	 $ throw $ ErrorSystemCmdFailed cmd code strOut strErr
	

-- | Run a successful system command, returning what it wrote to its @stdout@.
--   If anything was written to @stderr@ then treat that as failure. 
--   If it fails due to writing to @stderr@ or returning `ExitFailure`
--   then throw an error in the `Build` monad.
ssystemOut :: String -> Build String
ssystemOut cmd
 = do	(code, strOut, strErr)	<- systemTee True cmd ""
	when (code /= ExitSuccess || (not $ null strErr))
	 $ throw $ ErrorSystemCmdFailed cmd code strOut strErr

	return strOut

-- | Quietly run a successful system command, returning what it wrote to its @stdout@.
--   If anything was written to @stderr@ then treat that as failure. 
--   If it fails due to writing to @stderr@ or returning `ExitFailure`
--   then throw an error in the `Build` monad.
qssystemOut :: String -> Build String
qssystemOut cmd
 = do	(code, strOut, strErr)	<- systemTee False cmd ""
	when (code /= ExitSuccess || (not $ null strErr))
	 $ throw $ ErrorSystemCmdFailed cmd code strOut strErr

	return strOut



-- Tee versions -----------------------------------------------------------------------------------

-- | Like `systemTeeIO`, but in the `Build` monad.
systemTee 
	:: Bool 	
	-> String	
	-> String	
	-> Build (ExitCode, String, String)

systemTee tee cmd strIn
 = do	logSystem cmd
	io $ systemTeeIO tee cmd strIn


-- | Like `systemTeeIO`, but in the `Build` monad and throw an error if it returns `ExitFailure`.
ssystemTee  :: Bool -> String -> String -> Build ()
ssystemTee tee cmd strIn
 = do	logSystem cmd
	(code, strOut, strErr)	<- systemTee tee cmd strIn
	when (code /= ExitSuccess)
	 $ throw $ ErrorSystemCmdFailed cmd code strOut strErr

	

-- | Run a system command, returning its `ExitCode` and what was written to @stdout@ and @stderr@.
systemTeeIO 
	:: Bool 	-- ^ Whether @stdout@ and @stderr@ should be forwarded to the parent process.
	-> String 	-- ^ Command to run.
	-> String	-- ^ What to pass to the command's @stdin@.
	-> IO (ExitCode, String, String)

systemTeeIO tee cmd strIn
 = do	trace $ "systemTeeIO " ++ show tee ++ ": " ++ cmd

	-- Create some new pipes for the process to write its stdout and stderr to.
	trace $ "systemTeeIO: Creating process"
	(Just hInWrite, Just hOutRead, Just hErrRead, phProc)
	 	<- createProcess 
		$  CreateProcess
			{ cmdspec	= ShellCommand cmd
			, cwd		= Nothing
			, env		= Nothing
			, std_in	= CreatePipe
			, std_out	= CreatePipe
			, std_err	= CreatePipe
			, close_fds	= False }

	-- Push input into in handle
	hPutStr hInWrite strIn
			
	-- To implement the tee-like behavior we'll fork some threads that read lines from the
	-- processes stdout and stderr and write them to these channels. 
	-- 	When they hit EOF they signal this via the semaphores.
	chanOut		<- newChan
	chanErr		<- newChan
	semOut		<- newQSem 0
	semErr		<- newQSem 0

	-- Make duplicates of the above, which will store everything
	-- written to them. This gives us the copy to return from the fn.
	chanOutAcc	<- dupChan chanOut
	chanErrAcc	<- dupChan chanErr

	-- Fork threads to read from the process handles and write to our channels.
	_tidOut		<- forkIO $ streamIn hOutRead chanOut
	_tidErr		<- forkIO $ streamIn hErrRead chanErr

	-- If tee-like behavior is turned on, we forward what the process writes to
	--	its stdout and stderr to the parent.
	_tidStream	<- forkIO $ streamOuts
				[ (chanOut, if tee then Just stdout else Nothing, semOut)
				, (chanErr, if tee then Just stderr else Nothing, semErr) ]

	-- Wait for the main process to complete.
	code		<- waitForProcess phProc
	trace $ "systemTeeIO: Process done, code = " ++ show code

	trace $ "systemTeeIO: Waiting for sems"
	-- Wait for the tee processes to finish.
	--	We need to do this to avoid corrupted output on the console due to our forwarding
	--	threads writing at the same time as successing Build commands.
	mapM_ waitQSem [semOut, semErr]

	trace $ "systemTeeIO: Getting output"
	-- Get what was written to its stdout and stderr.
	--	getChanContents is a lazy read, so don't pull from the channel after
	--	seeing a Nothing else we'll block forever.
	strOut		<- liftM (concat . slurpUntilNothing) $ getChanContents chanOutAcc
	strErr		<- liftM (concat . slurpUntilNothing) $ getChanContents chanErrAcc

	trace $ "systemTeeIO stdout: " ++ strOut
	trace $ "systemTeeIO stderr: " ++ strErr

	trace $ "systemTeeIO: All done"
	code `seq` strOut `seq` strErr `seq` 
		return	(code, strOut, strErr)



slurpUntilNothing :: [Maybe a] -> [a]
slurpUntilNothing xx
 = case xx of
	[]		-> []
	Nothing : _	-> []
	Just x  : xs	-> x : slurpUntilNothing xs