{-# Language TemplateHaskell, OverloadedStrings, BangPatterns
, ScopedTypeVariables, TypeApplications #-}
module Client.State.DCC
(
DCCState(..)
, dsOffers
, dsTransfers
, emptyDCCState
, DCCOffer(..)
, dccNetwork
, dccFromInfo
, dccFromIP
, dccPort
, dccFileName
, dccSize
, dccOffset
, dccStatus
, DCCTransfer(..)
, dtThread
, dtProgress
, ConnectionStatus(..)
, DCCUpdate(..)
, supervisedDownload
, parseSEND
, parseACCEPT
, resumeMsg
, acceptUpdate
, getFileOffset
, insertAsNewMax
, ctcpToTuple
, statusAtKey
, reportStopWithStatus
, isSend
) where
import Control.Applicative (Alternative(..))
import Control.Concurrent.Async
import Control.Concurrent.STM
import Control.Exception (bracket, IOException)
import qualified Control.Exception as E
import Control.Lens hiding (from)
import Control.Monad (unless, when)
import Data.Attoparsec.Text
import qualified Data.ByteString as B
import Data.ByteString.Builder (word32BE, toLazyByteString)
import Data.ByteString.Lazy (toStrict)
import Data.IntMap (Key, IntMap)
import qualified Data.IntMap as I hiding (size, empty)
import Data.List (find)
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Word (Word32, Word64)
import Hookup
import Irc.Identifier (Identifier, idText)
import Irc.Message (IrcMsg(..))
import Irc.UserInfo (UserInfo(..), uiNick)
import Network.Socket ( HostName, PortNumber, Family(..)
, hostAddressToTuple )
import System.FilePath ((</>), takeFileName)
import System.IO (withFile, IOMode(..), openFile, hClose, hFileSize)
data DCCOffer = DCCOffer
{ _dccNetwork :: !Text
, _dccFromInfo :: !UserInfo
, _dccFromIP :: HostName
, _dccPort :: !PortNumber
, _dccFileName :: FilePath
, _dccSize :: !Word32
, _dccOffset :: !Word32
, _dccStatus :: !ConnectionStatus
} deriving (Show, Eq)
data ConnectionStatus
= CorrectlyFinished | UserKilled | LostConnection | Downloading | Pending
| NotExist
deriving (Eq, Show)
data DCCTransfer = DCCTransfer
{ _dtThread :: !(Maybe (Async ()))
, _dtProgress :: !Word32
}
data DCCState = DCCState
{ _dsOffers :: !(IntMap DCCOffer)
, _dsTransfers :: !(IntMap DCCTransfer)
}
data DCCUpdate
= PercentUpdate !Key !Word32
| Finished !Key
| SocketInterrupted !Key
| UserInterrupted !Key
| Accept !Key !PortNumber !Word32
deriving (Show, Eq)
makeLenses ''DCCOffer
makeLenses ''DCCTransfer
makeLenses ''DCCState
emptyDCCState :: DCCState
emptyDCCState = DCCState mempty mempty
dccOffer :: Text -> UserInfo -> HostName -> PortNumber
-> FilePath -> Word32 -> DCCOffer
dccOffer network userFrom hostaddr port filename filesize =
DCCOffer network userFrom hostaddr port filename filesize 0 Pending
supervisedDownload ::
FilePath ->
Key ->
TChan DCCUpdate ->
DCCState ->
IO DCCState
supervisedDownload dir key updChan state = do
let Just offer = view (dsOffers . at key) state
supervisorThread <- async $
withAsync (startDownload dir key updChan offer) $ \realTransferThread ->
do upd <- E.catches (Finished key <$ wait realTransferThread)
[ E.Handler (\(_ :: IOException) ->
return (SocketInterrupted key))
, E.Handler (\(_ :: AsyncCancelled) ->
return (UserInterrupted key))
]
atomically (writeTChan updChan upd)
let startPercent = percent (_dccOffset offer) (_dccSize offer)
newTransfer = DCCTransfer (Just supervisorThread) startPercent
newOffer = offer { _dccStatus = Downloading }
newState = set (dsOffers . at key) (Just newOffer)
$ set (dsTransfers . at key) (Just newTransfer) state
return newState
startDownload :: FilePath -> Key -> TChan DCCUpdate -> DCCOffer -> IO ()
startDownload dir key updChan offer@(DCCOffer _ _ from port name totalSize offset _) = do
let openMode = if offset > 0 then AppendMode else WriteMode
filepath = dir </> name
bracket (connect param) close $ \conn ->
bracket (openFile filepath openMode) hClose $ \hdl ->
do
recvChan1 <- atomically newTChan
recvChan2 <- atomically (dupTChan recvChan1)
withAsync (sendStream totalSize conn recvChan1)
$ \outThread -> withAsync (report offer key recvChan2 updChan)
$ \_reportThread -> do recvSendLoop offset recvChan1 conn hdl
wait outThread
where
param = ConnectionParams
{ cpFamily = AF_INET
, cpHost = from
, cpPort = port
, cpSocks = Nothing
, cpTls = Nothing }
buffSize = 4 * 1024 * 1024
recvSendLoop size chan conn hdl =
do bytes <- recv conn buffSize
unless (B.null bytes) $
do B.hPut hdl bytes
let newSize = size + fromIntegral (B.length bytes)
atomically (writeTChan chan newSize)
recvSendLoop newSize chan conn hdl
sendStream :: Word32 -> Connection -> TChan Word32 -> IO ()
sendStream maxSize conn chan =
do val <- atomically (readTChan chan)
let valBE = toStrict (toLazyByteString (word32BE val))
send conn valBE
unless (val >= maxSize) (sendStream maxSize conn chan)
report :: DCCOffer -> Key -> TChan Word32 -> TChan DCCUpdate -> IO ()
report offer key input output = compareAndUpdate (percent offset totalsize)
where
offset = _dccOffset offer
totalsize = _dccSize offer
compareAndUpdate :: Word32 -> IO ()
compareAndUpdate prevPercent =
do curSize <- atomically $ readTChan input
let curPercent = percent curSize totalsize
updateEv = PercentUpdate key curPercent
if curPercent == 100
then atomically (writeTChan output updateEv)
else do when (curPercent > prevPercent)
(atomically (writeTChan output updateEv))
compareAndUpdate curPercent
percent :: Word32 -> Word32 -> Word32
percent a total = fromIntegral (fromIntegral a * 100 `div` fromIntegral total :: Word64)
reportStopWithStatus :: Key -> ConnectionStatus -> DCCState -> DCCState
reportStopWithStatus key newstatus
= set (dsOffers . ix key . dccStatus) newstatus
. set (dsTransfers . ix key . dtThread ) Nothing
parseSEND :: Text -> UserInfo -> Text -> Either String DCCOffer
parseSEND network userFrom = parseOnly (sendFormat network userFrom)
sendFormat :: Text -> UserInfo -> Parser DCCOffer
sendFormat network userFrom =
do name <- string "SEND" *> space *> nameFormat
addr <- ipv4Dotted <$ space <*> decimal
port <- space *> decimal
totalsize <- space *> decimal
return (dccOffer network userFrom addr port name totalsize)
parseACCEPT :: DCCState -> UserInfo -> Text -> Maybe DCCUpdate
parseACCEPT state userFrom text =
case parseOnly acceptFormat text of
Left _ -> Nothing
Right (fileName, port, offset) ->
do (key, _) <- find (predicate fileName) offerList
return (Accept key port offset)
where
offerList = I.toDescList (_dsOffers state)
predicate fileName (key, offer) =
view dccFileName offer == fileName &&
view dccFromInfo offer == userFrom &&
statusAtKey key state == Pending
acceptFormat :: Parser (FilePath, PortNumber, Word32)
acceptFormat =
do filepath <- string "ACCEPT" *> space *> nameFormat
port <- space *> decimal
offset <- space *> decimal
return (filepath, port, offset)
nameFormat :: Parser FilePath
nameFormat = do textPath <- try quotedName <|> noSpaceName
return (takeFileName (Text.unpack textPath))
where
quotedName = char '\"' *> takeWhile1 ('\"' /=) <* char '\"'
noSpaceName = takeWhile1 (' ' /=)
ipv4Dotted :: Word32 -> HostName
ipv4Dotted addr = ipv4Format (bigToLittleEndian (hostAddressToTuple addr))
where
bigToLittleEndian (a, b, c, d) = (d, c, b, a)
ipv4Format (d,c,b,a) =
show d <> "." <> show c <> "." <> show b <> "." <> show a
getFileOffset :: FilePath -> IO (Maybe Word32)
getFileOffset path =
do res <- E.try (withFile path ReadMode hFileSize)
return $! case res :: Either IOError Integer of
Right n | n > 0 -> Just $! fromIntegral n
_ -> Nothing
insertAsNewMax :: DCCOffer -> DCCState -> DCCState
insertAsNewMax newoffer (DCCState offers transfers) =
let newmax = if I.null offers then 1 else 1 + fst (I.findMax offers)
newOffers = I.insert newmax newoffer offers
in DCCState newOffers transfers
ctcpToTuple :: IrcMsg -> Maybe (UserInfo, Identifier, Text, Text)
ctcpToTuple (Ctcp fromU target command txt) =
Just (fromU, target, command, txt)
ctcpToTuple (CtcpNotice fromU target command txt) =
Just (fromU, target, command, txt)
ctcpToTuple _ = Nothing
statusAtKey :: Key -> DCCState -> ConnectionStatus
statusAtKey key (DCCState offers _) =
case I.lookup key offers of
Nothing -> NotExist
Just d -> view dccStatus d
resumeMsg ::
Word32 ->
DCCOffer ->
(String, String)
resumeMsg sizeoffset offer = (target, txt)
where
filename = _dccFileName offer
port = show (_dccPort offer)
sizeoffset' = show sizeoffset
quoting = if ' ' `elem` filename then "\"" else ""
txt = concat ["RESUME ", quoting, filename, quoting,
" ", port, " ", sizeoffset' ]
target = views (dccFromInfo . uiNick) (Text.unpack . idText) offer
acceptUpdate :: DCCUpdate -> DCCState -> DCCState
acceptUpdate (Accept k port offset) state =
case view (dsOffers . at k) state of
Nothing -> state
Just oldOffer -> set (dsOffers . at k) (Just newOffer) state
where
newOffer = oldOffer { _dccPort = port, _dccOffset = offset }
acceptUpdate _ state = state
isSend :: Text -> Bool
isSend txt
| "SEND":_ <- Text.splitOn " " txt = True
| otherwise = False