libssh2-conduit-0.1: Network/SSH/Client/LibSSH2/Conduit.hs
{-# LANGUAGE FlexibleContexts #-}
module Network.SSH.Client.LibSSH2.Conduit
(sourceChannel,
splitLines,
CommandsHandle,
execCommand,
getReturnCode
) where
import Control.Monad
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Resource
import Control.Monad.Trans.Control
import System.IO.Unsafe (unsafeInterleaveIO)
import Control.Concurrent.STM
import Data.Monoid
import Data.Conduit
import Data.Conduit.Lazy
import Network.SSH.Client.LibSSH2.Foreign
import Network.SSH.Client.LibSSH2
-- | Read all contents of libssh2's Channel.
sourceChannel :: Channel -> Source IO String
sourceChannel ch = src
where
src = Source pull close
pull = do
(sz, res) <- liftIO $ readChannel ch 0x400
if sz > 0
then return $ Open src res
else return Closed
close = return ()
-- | Similar to Data.Conduit.Binary.lines, but for Strings.
splitLines :: Resource m => Conduit String m String
splitLines =
conduitState id push close
where
push front bs' = return $ StateProducing leftover ls
where
bs = front bs'
(leftover, ls) = getLines id bs
getLines front bs
| null bs = (id, front [])
| null y = ((x ++), front [])
| otherwise = getLines (front . (x:)) (drop 1 y)
where
(x, y) = break (== '\n') bs
close front
| null bs = return []
| otherwise = return [bs]
where
bs = front ""
-- | Execute one command and read it's output lazily.
-- If first argument is True, then you *must* get return code
-- using getReturnCode on returned CommandsHandle. Moreover,
-- you *must* guarantee that getReturnCode will be called
-- only when all command output will be read.
execCommand :: Bool -- ^ Set to True if you want to get return code when command will terminate.
-> Session
-> String -- ^ Command
-> IO (Maybe CommandsHandle, Source IO String)
execCommand b s cmd = do
(ch, channel) <- initCH b s
let src = execCommandS ch channel cmd $= splitLines
return (if b then Just ch else Nothing, src)
-- execCommands :: Bool -> Session -> [String] -> IO [String]
-- execCommands b s cmds = do
-- let srcs = [execCommandS (v i) s cmd | (i, cmd) <- zip [1..] cmds ]
-- v i | i == length cmds = var
-- | otherwise = Nothing
-- res <- runResourceT $ lazyConsume $ mconcat srcs $= splitLines
-- return res
-- | Handles channel opening and closing.
data CommandsHandle = CommandsHandle {
chReturnCode :: Maybe (TMVar Int),
chChannel :: TMVar Channel,
chChannelClosed :: TVar Bool }
initCH :: Bool -> Session -> IO (CommandsHandle, Channel)
initCH False s = do
c <- newTVarIO False
ch <- newEmptyTMVarIO
channel <- openCH ch s
return (CommandsHandle Nothing ch c, channel)
initCH True s = do
r <- newEmptyTMVarIO
c <- newTVarIO False
ch <- newEmptyTMVarIO
channel <- openCH ch s
return (CommandsHandle (Just r) ch c, channel)
openCH :: TMVar Channel -> Session -> IO Channel
openCH var s = do
ch <- openChannelSession s
atomically $ putTMVar var ch
return ch
-- | Get return code for previously run command.
-- It will fail if command was run using execCommand False.
-- Should be called only when all command output is read.
getReturnCode :: CommandsHandle -> IO Int
getReturnCode ch = do
c <- atomically $ readTVar (chChannelClosed ch)
if c
then do
case chReturnCode ch of
Nothing -> fail "Channel already closed and no exit code return was set up for command."
Just v -> atomically $ takeTMVar v
else do
channel <- atomically $ takeTMVar (chChannel ch)
cleanupChannel ch channel
atomically $ writeTVar (chChannelClosed ch) True
case chReturnCode ch of
Nothing -> fail "No exit code return was set up for commnand."
Just v -> do
rc <- atomically $ takeTMVar v
return rc
execCommandS :: CommandsHandle -> Channel -> String -> Source IO String
execCommandS var channel command =
Source {
sourcePull = pull channel
, sourceClose = return () }
where
next ch =
Source (pullAnswer ch) $ do
return ()
--liftIO $ cleanupChannel var ch
pullAnswer ch = do
(sz, res) <- liftIO $ readChannel ch 0x400
if sz > 0
then return $ Open (next ch) res
else do
liftIO $ cleanupChannel var ch
return Closed
pull ch = do
liftIO $ channelExecute ch command
pullAnswer ch
-- | Close Channel and write return code
cleanupChannel :: CommandsHandle -> Channel -> IO ()
cleanupChannel ch channel = do
c <- atomically $ readTVar (chChannelClosed ch)
when (not c) $ do
closeChannel channel
case chReturnCode ch of
Nothing -> return ()
Just v -> do
exitStatus <- channelExitStatus channel
atomically $ putTMVar v exitStatus
closeChannel channel
freeChannel channel
atomically $ writeTVar (chChannelClosed ch) True
return ()