{-# OPTIONS_GHC -XFlexibleInstances -XTypeSynonymInstances #-}

{- Commands for HSH
Copyright (C) 2004-2008 John Goerzen <jgoerzen@complete.org>
Please see the COPYRIGHT file
-}

{- |
   Module     : HSH.Command
   Copyright  : Copyright (C) 2006-2008 John Goerzen
   License    : GNU LGPL, version 2.1 or above

   Maintainer : John Goerzen <jgoerzen@complete.org>
   Stability  : provisional
   Portability: portable

Copyright (c) 2006-2007 John Goerzen, jgoerzen\@complete.org
-}

module HSH.Command (ShellCommand(..),
                    PipeCommand(..),
                    (-|-),
                    RunResult,
                    run,
                    runIO,
                    runSL,
                    InvokeResult,
                    tryEC,
                    catchEC,
                   ) where

-- import System.IO.HVIO
-- import System.IO.Utils
import System.IO
import System.Exit
import System.Posix.Types
import System.Posix.IO
import System.Posix.Process
import System.Log.Logger
import System.IO.Error
import Data.Maybe.Utils
import Data.Maybe
import Data.List.Utils(uniq)
import Control.Exception(evaluate)
import System.Posix.Env
import Text.Regex.Posix
import Control.Monad(when)
import Data.String.Utils(rstrip)
import qualified Data.ByteString.Lazy as BSL
import qualified Data.ByteString as BS

d, dr :: String -> IO ()
d = debugM "HSH.Command"
dr = debugM "HSH.Command.Run"

{- | Result type for shell commands.  The String is the text description of
the command, not its output. -}
type InvokeResult = (String, IO ProcessStatus)

{- | A shell command is something we can invoke, pipe to, pipe from,
or pipe in both directions.  All commands that can be run as shell
commands must define these methods.

Minimum implementation is 'fdInvoke'.
-}
class (Show a) => ShellCommand a where
    {- | Invoke a command. -}
    fdInvoke :: a               -- ^ The command
             -> Fd              -- ^ fd to pass to it as stdin
             -> Fd              -- ^ fd to pass to it as stdout
             -> [Fd]            -- ^ Fds to close in the child.  Will not harm Fds this child needs for stdin and stdout.
             -> (IO ())           -- ^ Action to run post-fork in child (or in main process if it doesn't fork)
             -> IO [InvokeResult]           -- ^ Returns an action that, when evaluated, waits for the process to finish and returns an exit code.

instance Show (String -> String) where
    show _ = "(String -> String)"
instance Show (String -> IO String) where
    show _ = "(String -> IO String)"
instance Show (BSL.ByteString -> BSL.ByteString) where
    show _ = "(Data.ByteString.Lazy.ByteString -> Data.ByteString.Lazy.ByteString)"
instance Show (BSL.ByteString -> IO BSL.ByteString) where
    show _ = "(Data.ByteString.Lazy.ByteString -> IO Data.ByteString.Lazy.ByteString)"
instance Show (BS.ByteString -> BS.ByteString) where
    show _ = "(Data.ByteString.ByteString -> Data.ByteString.ByteString)"
instance Show (BS.ByteString -> IO BS.ByteString) where
    show _ = "(Data.ByteString.ByteString -> IO Data.ByteString.ByteString)"

instance ShellCommand (String -> IO String) where
    fdInvoke = genericStringlikeIO hGetContents hPutStr

instance ShellCommand (BSL.ByteString -> IO BSL.ByteString) where
    fdInvoke = genericStringlikeIO BSL.hGetContents BSL.hPut

instance ShellCommand (BS.ByteString -> IO BS.ByteString) where
    fdInvoke = genericStringlikeIO BS.hGetContents BS.hPut

{- | An instance of 'ShellCommand' for a pure Haskell function mapping
String to String.  Implement in terms of (String -> IO String) for
simplicity. -}
instance ShellCommand (String -> String) where
    fdInvoke func =
        fdInvoke iofunc
            where iofunc :: String -> IO String
                  iofunc = return . func

instance ShellCommand (BSL.ByteString -> BSL.ByteString) where
    fdInvoke func =
        fdInvoke iofunc
            where iofunc :: BSL.ByteString -> IO BSL.ByteString
                  iofunc = return . func

instance ShellCommand (BS.ByteString -> BS.ByteString) where
    fdInvoke func =
        fdInvoke iofunc
            where iofunc :: BS.ByteString -> IO BS.ByteString
                  iofunc = return . func

genericStringlikeIO :: (Show (a -> IO a)) => 
                       (Handle -> IO a) 
                    -> (Handle -> a -> IO ()) 
                    -> (a -> IO a) 
                    -> Fd
                    -> Fd 
                    -> [Fd] 
                    -> (IO ()) 
                    -> IO [InvokeResult]
genericStringlikeIO getcontentsfunc hputstrfunc func fstdin fstdout childclosefds childfunc =
        do -- d $ "SIOSF: Before fork"
           p <- try (forkProcess childstuff)
           pid <- case p of
                    Right x -> return x
                    Left x -> fail $ "Error in fork for func: " ++ show x
           -- d $ "SIOSFP: New func pid " ++ show pid
           return $ seq pid pid
           return [(show func,
                   getProcessStatus True False pid >>=
                                    (return . forceMaybe))]
        where childstuff = do closefds childclosefds [fstdin, fstdout]
                              d $ "SIOSFC Input is on " ++ show fstdin
                              hr <- fdToHandle fstdin
                              d $ "SIOSFC Output is on " ++ show fstdout
                              hw <- fdToHandle fstdout
                              hSetBuffering hw LineBuffering
                              d $ "SIOSFC Running child func"
                              childfunc
                              d $ "SIOSFC Running func in child"
                              contents <- getcontentsfunc hr
                              d $ "SIOSFC Contents read"
                              result <- func contents
                              d $ "SIOSFC Func applied"
                              hputstrfunc hw result
                              d $ "SIOSFC Func done, closing handles."
                              hClose hr
                              hClose hw
                              d $ "SIOSFC Child exiting."
                              -- It hung here without the exitImmediately
                              --exitImmediately ExitSuccess

instance Show ([String] -> [String]) where
    show _ = "([String] -> [String])"
instance Show ([String] -> IO [String]) where
    show _ = "([String] -> IO [String])"

{- | An instance of 'ShellCommand' for a pure Haskell function mapping
[String] to [String].

A [String] is generated from a Handle via the 'lines' function, and the
reverse occurs via 'unlines'.

So, this function is intended to operate upon lines of input and produce
lines of output. -}
instance ShellCommand ([String] -> [String]) where
    fdInvoke func = fdInvoke (unlines . func . lines)

{- | The same for an IO function -}
instance ShellCommand ([String] -> IO [String]) where
    fdInvoke func = fdInvoke iofunc
        where iofunc input = do r <- func (lines input)
                                return (unlines r)


{- | An instance of 'ShellCommand' for an external command.  The
first String is the command to run, and the list of Strings represents the
arguments to the program, if any. -}
instance ShellCommand (String, [String]) where
    fdInvoke pc@(cmd, args) fstdin fstdout childclosefds childfunc =
        do d $ "S Before fork for " ++ show pc
           p <- try (forkProcess childstuff)
           pid <- case p of
                    Right x -> return x
                    Left x -> fail $ "Error in fork: " ++ show x
           d $ "SP New pid " ++ show pid ++ " for " ++ show pc
           return $ seq pid pid
           return [(show (cmd, args),
                   getProcessStatus True False pid >>=
                                        (return . forceMaybe))]

        where
              childstuff = do d $ "SC preparing to redir"
                              d $ "SC input is on " ++ show fstdin
                              d $ "SC output is on " ++ show fstdout
                              redir fstdin stdInput
                              redir fstdout stdOutput
                              closefds childclosefds [fstdin, fstdout, 0, 1]
                              childfunc
                              dr ("RUN: " ++ cmd ++ " " ++ (show args))
                              executeFile cmd True args Nothing

{- | An instance of 'ShellCommand' for an external command.  The
String is split using words to the command to run, and the arguments, if any. -}
instance ShellCommand String where
    fdInvoke cmdline ifd ofd closefd forkfunc =
        do esh <- getEnv "SHELL"
           let sh = case esh of
                      Nothing -> "/bin/sh"
                      Just x -> x
           fdInvoke (sh, ["-c", cmdline]) ifd ofd closefd forkfunc

redir :: Fd -> Fd -> IO ()
redir fromfd tofd
    | fromfd == tofd = do d $ "ignoring identical redir " ++ show fromfd
                          return ()
    | otherwise = do d $ "running dupTo " ++ show (fromfd, tofd)
                     dupTo fromfd tofd
                     closeFd fromfd

closefds :: [Fd]                   -- ^ List of Fds to possibly close
         -> [Fd]                   -- ^ List of Fds to not touch, ever
         -> IO ()
closefds inpclosefds noclosefds =
    do d $ "closefds " ++ show uclosefds ++ " " ++ show noclosefds
       mapM_ closeit . filter (\x -> not (x `elem` noclosefds)) $ uclosefds
    where closeit fd = do d $ "Closing fd " ++ show fd
                          closeFd fd
          uclosefds = uniq inpclosefds

data (ShellCommand a, ShellCommand b) => PipeCommand a b = PipeCommand a b
   deriving Show

{- | An instance of 'ShellCommand' represeting a pipeline. -}
instance (ShellCommand a, ShellCommand b) => ShellCommand (PipeCommand a b) where
    fdInvoke pc@(PipeCommand cmd1 cmd2) fstdin fstdout childclosefds forkfunc =
        do d $ "*** Handling pipe: " ++ show pc
           (reader, writer) <- createPipe
           let allfdstoclose = reader : writer : fstdin : fstdout : childclosefds
           d $ "pipd fdInvoke: New pipe endpoints: " ++ show (reader, writer)
           res1 <- fdInvoke cmd1 fstdin writer allfdstoclose forkfunc
           res2 <- fdInvoke cmd2 reader fstdout allfdstoclose forkfunc
           d $ "pipe fdInvoke: Parent closing " ++ show [reader, writer]
           mapM_ closeFd [reader, writer]

           d $ "*** Done handling pipe " ++ show pc
           return $ res1 ++ res2

{- | Pipe the output of the first command into the input of the second. -}
(-|-) :: (ShellCommand a, ShellCommand b) => a -> b -> PipeCommand a b
(-|-) = PipeCommand

{- | Function to use when there is nothing for the child to do -}
nullChildFunc :: IO ()
nullChildFunc = return ()

{- | Different ways to get data from 'run'.

 * IO () runs, throws an exception on error, and sends stdout to stdout

 * IO String runs, throws an exception on error, reads stdout into
   a buffer, and returns it as a string.  Note: This output is not lazy.

 * IO [String] is same as IO String, but returns the results as lines.
   Note: this output is not lazy.

 * IO ProcessStatus runs and returns a ProcessStatus with the exit
   information.  stdout is sent to stdout.  Exceptions are not thrown.

 * IO (String, ProcessStatus) is like IO ProcessStatus, but also
   includes a description of the last command in the pipe to have
   an error (or the last command, if there was no error)

 * IO ByteString and IO (ByteString, ProcessStatus) are similar to their
   String counterparts.

 * IO Int returns the exit code from a program directly.  If a signal
   caused the command to be reaped, returns 128 + SIGNUM.

 * IO Bool returns True if the program exited normally (exit code 0,
   not stopped by a signal) and False otherwise.

To address insufficient laziness, you can process anything that needs to be
processed lazily within the pipeline itself.
-}
class RunResult a where
    {- | Runs a command (or pipe of commands), with results presented
       in any number of different ways. -}
    run :: (ShellCommand b) => b -> a

instance RunResult (IO ()) where
    run cmd = run cmd >>= checkResults

instance RunResult (IO (String, ProcessStatus)) where
    run cmd =
        do r <- fdInvoke cmd stdInput stdOutput [] nullChildFunc
           processResults r

instance RunResult (IO ProcessStatus) where
    run cmd = ((run cmd)::IO (String, ProcessStatus)) >>= return . snd

instance RunResult (IO Int) where
    run cmd = do rc <- run cmd
                 case rc of
                   Exited (ExitSuccess) -> return 0
                   Exited (ExitFailure x) -> return x
                   Terminated x -> return (128 + (fromIntegral x))
                   Stopped x -> return (128 + (fromIntegral x))

instance RunResult (IO Bool) where
    run cmd = do rc <- run cmd
                 return ((rc::Int) == 0)

instance RunResult (IO [String]) where
    run cmd = do r <- run cmd
                 return (lines r)

instance RunResult (IO String) where
    run cmd = genericStringlikeResult hGetContents (\c -> evaluate (length c))
              cmd

instance RunResult (IO BSL.ByteString) where
    run cmd = genericStringlikeResult BSL.hGetContents 
              (\c -> evaluate (BSL.length c))
              cmd

instance RunResult (IO BS.ByteString) where
    run cmd = genericStringlikeResult BS.hGetContents
              (\c -> evaluate (BS.length c))
              cmd

genericStringlikeResult :: ShellCommand b => 
                           (Handle -> IO a)
                        -> (a -> IO c)
                        -> b 
                        -> IO a
genericStringlikeResult hgetcontentsfunc evalfunc cmd =
        do (pread, pwrite) <- createPipe
           -- d $ "runS: new pipe endpoints: " ++ show [pread, pwrite]
           -- d "runS 1"
           r <- fdInvoke cmd stdInput pwrite [pread, pwrite] nullChildFunc
           -- d $ "runS 2 closing " ++ show pwrite
           closeFd pwrite
           -- d "runS 3"
           hread <- fdToHandle pread
           -- d "runS 4"
           c <- hgetcontentsfunc hread
           -- d "runS 5"
           evalfunc c
           --evaluate (length c)
           -- d "runS 6"
           hClose hread
           -- d "runS 7"
           processResults r >>= checkResults
           -- d "runS 8"
           return c

{- | Evaluates the result codes and returns an overall status -}
processResults :: [InvokeResult] -> IO (String, ProcessStatus)
processResults r =
    do rc <- mapM procresult r
       case catMaybes rc of
         [] -> return (fst (last r), Exited (ExitSuccess))
         x -> return (last x)
    where procresult :: InvokeResult -> IO (Maybe (String, ProcessStatus))
          procresult (cmd, action) =
              do rc <- action
                 return $ case rc of
                   Exited (ExitSuccess) -> Nothing
                   x -> Just (cmd, x)

{- | Evaluates result codes and raises an error for any bad ones it finds. -}
checkResults :: (String, ProcessStatus) -> IO ()
checkResults (cmd, ps) =
       case ps of
         Exited (ExitSuccess) -> return ()
         Exited (ExitFailure x) ->
             fail $ cmd ++ ": exited with code " ++ show x
         Terminated sig ->
             fail $ cmd ++ ": terminated by signal " ++ show sig
         Stopped sig ->
             fail $ cmd ++ ": stopped by signal " ++ show sig

{- | Handle an exception derived from a program exiting abnormally -}
tryEC :: IO a -> IO (Either ProcessStatus a)
tryEC action =
    do r <- try action
       case r of
         Left ioe ->
          if isUserError ioe then
              case (ioeGetErrorString ioe =~~ pat) of
                Nothing -> ioError ioe -- not ours; re-raise it
                Just e -> return . Left . proc $ e
          else ioError ioe      -- not ours; re-raise it
         Right result -> return (Right result)
    where pat = ": exited with code [0-9]+$|: terminated by signal ([0-9]+)$|: stopped by signal [0-9]+"
          proc :: String -> ProcessStatus
          proc e
              | e =~ "^: exited" = Exited (ExitFailure (str2ec e))
              | e =~ "^: terminated by signal" = Terminated (str2ec e)
              | e =~ "^: stopped by signal" = Stopped (str2ec e)
              | otherwise = error "Internal error in tryEC"
          str2ec e =
              read (e =~ "[0-9]+$")

{- | Catch an exception derived from a program exiting abnormally -}
catchEC :: IO a -> (ProcessStatus -> IO a) -> IO a
catchEC action handler =
    do r <- tryEC action
       case r of
         Left ec -> handler ec
         Right result -> return result

{- | A convenience function.  Refers only to the version of 'run'
that returns @IO ()@.  This prevents you from having to cast to it
all the time when you do not care about the result of 'run'.

The implementation is simply:

>runIO :: (ShellCommand a) => a -> IO ()
>runIO = run
-}
runIO :: (ShellCommand a) => a -> IO ()
runIO = run

{- | Another convenience function.  This returns the first line of the output,
with any trailing newlines or whitespace stripped off.  No leading whitespace
is stripped.  This function will raise an exception if there is not at least
one line of output.  Mnemonic: runSL means \"run single line\".

This command exists separately from 'run' because there is already a
'run' instance that returns a String, though that instance returns the
entirety of the output in that String. -}
runSL :: (ShellCommand a) => a -> IO String
runSL cmd =
    do r <- run cmd
       when (r == []) $ fail $ "runSL: no output received from " ++ show cmd
       return (rstrip . head $ r)