packages feed

handsy-0.0.14.1: src/System/Handsy/Actions.hs

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE OverloadedStrings #-}

module System.Handsy.Actions where

import           Control.Applicative
import           Control.Concurrent
import           Control.Error
import           Control.Monad.IO.Class
import           Control.Monad.Trans.Class
import qualified Data.ByteString.Char8      as B8
import qualified Data.ByteString.Lazy       as BL
import qualified Data.ByteString.Lazy.Char8 as BL8
import           Data.Default.Class
import           Data.List.Split
import           Prelude                    hiding (appendFile, readFile,
                                             writeFile)
import           System.Exit
import           System.Handsy.Internal
import           Text.ShellEscape

-- | Runs a command
command :: FilePath     -- ^ Command to run
        -> [String]     -- ^ Arguments
        -> CommandOptions
        -> Handsy (ExitCode, BL.ByteString, BL.ByteString) -- ^ (status, stdout, stderr)
command cmd args opts = let cmd' = B8.unpack . B8.intercalate " " . map (bytes . bash . B8.pack) $ (cmd:args)
                        in  shell cmd' opts

{-| Executes the given string in shell. Example:

  > shell "ls" $~ def{cwd="/var/www"}
-}
shell :: String       -- ^ String to execute
      -> CommandOptions
      -> Handsy (ExitCode, BL.ByteString, BL.ByteString) -- ^ (ExitCode, Stdout, Stderr)
shell cmd opts = let esc = B8.unpack . bytes . bash . B8.pack
                     CommandOptions stdin' cwd' = opts
                 in  shellF ((if null cwd' then "" else ("cd " ++ esc cwd' ++ "; ")) ++ cmd) stdin'

data CommandOptions =
  CommandOptions { stdin :: BL.ByteString
                 , cwd   :: String
                 }

instance Default CommandOptions where
  def = CommandOptions "" ""

-- | Reads a file and returns the contents of the file.
readFile :: FilePath -> Handsy BL.ByteString
readFile fp = command "cat" [fp] def >>= \case
  (ExitSuccess, sout, _) -> return sout
  (_, _, serr)           -> lift . left $ "Serror reading " ++ fp ++ "\nSerr was: " ++ BL8.unpack serr

-- | @writeFile file str@ function writes the bytestring @str@, to the file @file@.
writeFile :: FilePath -> BL.ByteString -> Handsy ()
writeFile fp s = command "dd" ["of=" ++ fp] def{stdin=s} >>= \case
  (ExitSuccess, _, _) -> return ()
  (_, _, serr)      -> lift . left $ "Serror writing to " ++ fp  ++ "\nSerr was: " ++ BL8.unpack serr

-- | @appendFile file str@ function appends the bytestring @str@, to the file @file@.
appendFile :: FilePath -> BL.ByteString -> Handsy ()
appendFile fp s = command "dd" ["of=" ++ fp, "conv=notrunc", "oflag=append"] def{stdin=s} >>= \case
  (ExitSuccess, _, _) -> return ()
  (_, _, serr)      -> lift . left $ "Serror appending to " ++ fp ++ "\nSerr was: " ++ BL8.unpack serr

-- | Same as 'command', but ExitFailure ends the computation.
command_ :: FilePath -> [String] -> CommandOptions -> Handsy (BL.ByteString, BL.ByteString)
command_ path args opts = command path args opts >>= \case
  (ExitFailure code, _, serr) -> lift $ left ('`':path ++ ' ' : show args ++ "` returned " ++ show code
                                                ++ "\nSerr was: " ++ BL8.unpack serr)
  (ExitSuccess, sout, serr) -> return (sout, serr)

-- | Same as 'shell', but ExitFailure ends the computation.
shell_ :: String -> CommandOptions -> Handsy (BL.ByteString, BL.ByteString)
shell_ cmd opts = shell cmd opts >>= \case
  (ExitFailure code, _, serr) -> lift $ left ('`':cmd ++ "` returned " ++ show code
                                                ++ "\nSerr was: " ++ BL8.unpack serr)
  (ExitSuccess, sout, serr) -> return (sout, serr)

-- * Helpers for parsing return values of 'command' and 'shell'

class IsReturnValue a where
  stdout   :: a -> BL.ByteString
  stderr   :: a -> BL.ByteString
  exitCode :: a -> ExitCode

instance IsReturnValue (BL.ByteString, BL.ByteString) where
  stdout   = fst
  stderr   = snd
  exitCode = const ExitSuccess

instance IsReturnValue (ExitCode, BL.ByteString, BL.ByteString) where
  stdout   = \case (_, a, _) -> a
  stderr   = \case (_, _, a) -> a
  exitCode = \case (a, _, _) -> a

isSuccessful :: IsReturnValue a => a -> Bool
isSuccessful = isExitSuccess . exitCode

isExitSuccess :: ExitCode -> Bool
isExitSuccess = \case
  ExitSuccess   -> True
  ExitFailure _ -> False

-- | Extract lines from a ByteString. Useful for parsing unix commands.
strLines :: BL.ByteString -> [String]
strLines = lines . BL8.unpack

-- | Waits specified number of seconds
sleep :: Int -> Handsy ()
sleep = liftIO . threadDelay . (* 1000000)

-- | Creates a temporary file
mkTemp :: String -> Handsy String
mkTemp suffix = do
  out <- strLines .stdout <$> command_ "mktemp" (bool ["--suffix=" ++ suffix] [] (null suffix)) def
  lift $ tryHead ("weird output from mktemp: " ++ show out) out

-- | Creates a temporary directory
mkTempDir :: String -> Handsy String
mkTempDir suffix
  = command_ "mktemp" ("-d" : bool ["--suffix" ++ suffix] [] (null suffix)) def
    >>= lift . tryHead "weird output from mktemp" . strLines . stdout

-- | Returns if the specified process is running. Uses `pidof`
isRunning :: String -> Handsy Bool
isRunning p = isSuccessful <$> command "pidof" ["-s", "-x", p] def

data OS = NixOS | Debian | Ubuntu | RHEL | CentOS | Fedora | ArchLinux
  deriving (Show, Eq)

{-| Guesses the os using `/etc/os-release`. This currently only supports Linux distributions
    abiding systemd standards. -}
os :: Handsy (Maybe OS)
os = parseOsRelease <$> readFile "/etc/os-release" >>= return . \case
    Just "ubuntu" -> Just Ubuntu
    Just "debian" -> Just Debian
    Just "nixos"  -> Just NixOS
    Just "rhel"   -> Just RHEL
    Just "centos" -> Just CentOS
    Just "fedora" -> Just Fedora
    Just "arch"   -> Just ArchLinux
    _             -> Nothing
  where parseOsRelease = fmap (filter $ not . flip elem ['\'', '"']) -- Hack to unquote
          <$> lookup "ID" . map ((\(x:xs) -> (x, concat xs)) . splitOn "=") . strLines