HaskellTorrent (empty) → 0.0
raw patch · 30 files changed
+3922/−0 lines, 30 filesdep +HTTPdep +HsOpenSSLdep +basesetup-changed
Dependencies added: HTTP, HsOpenSSL, base, bytestring, cereal, cml, containers, directory, mtl, network, parsec, pretty, random, random-shuffle, time
Files
- AUTHORS +5/−0
- HaskellTorrent.cabal +74/−0
- LICENSE +25/−0
- README.md +92/−0
- Setup.lhs +7/−0
- configure +21/−0
- src/Data/Queue.hs +42/−0
- src/Digest.hs +32/−0
- src/FS.hs +226/−0
- src/HaskellTorrent.hs +85/−0
- src/Logging.hs +40/−0
- src/LoggingTypes.hs +36/−0
- src/PeerTypes.hs +30/−0
- src/Process.hs +211/−0
- src/Process/ChokeMgr.hs +358/−0
- src/Process/Console.hs +81/−0
- src/Process/FS.hs +112/−0
- src/Process/Listen.hs +34/−0
- src/Process/Peer.hs +460/−0
- src/Process/PeerMgr.hs +95/−0
- src/Process/PieceMgr.hs +432/−0
- src/Process/Status.hs +116/−0
- src/Process/Timer.hs +25/−0
- src/Process/Tracker.hs +269/−0
- src/Protocol/BCode.hs +365/−0
- src/Protocol/Wire.hs +227/−0
- src/RateCalc.hs +76/−0
- src/Supervisor.hs +177/−0
- src/Torrent.hs +161/−0
- src/Version.hs.in +8/−0
+ AUTHORS view
@@ -0,0 +1,5 @@+Alex Mason <axman6@gmail.com>+Astro <astro@spaceboyz.net>+Jesper Louis Andersen <jesper.louis.andersen@gmail.com>+John Gunderman <johngunderman@gmail.com>+Thomas Christensen <thomas@chrstnsn.dk>
+ HaskellTorrent.cabal view
@@ -0,0 +1,74 @@+name: HaskellTorrent+category: Network+version: 0.0+category: Network+description: HaskellTorrent provides a BitTorrent client, based on the CML library+ for concurrency. This is an early preview release which is capable of+ downloading files from various torrent trackers, but have not yet+ demonstrated to be correct in all aspects.++ It is expected that the package currently contains numerous and even+ grave bugs. Patches to fix any problem are welcome!+cabal-version: >= 1.6++license: BSD3+license-file: LICENSE+copyright: (c) 2009 Jesper Louis Andersen+author: Jesper Louis Andersen+maintainer: jesper.louis.andersen@gmail.com+stability: experimental+synopsis: A concurrent bittorrent client+build-type: Configure++extra-tmp-files: src/Version.hs+extra-source-files: src/Version.hs.in, configure+data-files: AUTHORS, README.md++flag debug+ description: Enable debug support+ default: False++executable HaskellTorrent+ hs-source-dirs: src+ main-is: HaskellTorrent.hs+ other-modules: Protocol.BCode, Protocol.Wire,+ Data.Queue,+ Process.ChokeMgr, Process.Console, Process.FS, Process.Listen,+ Process.PeerMgr, Process.Peer, Process.PieceMgr, Process.Status,+ Process.Timer, Process.Tracker,+ Digest, FS, Logging, LoggingTypes, PeerTypes, Process, RateCalc,+ Supervisor, Torrent++ extensions: CPP++ build-depends:+ base >= 3.0,+ base <= 5.0,+ mtl,+ cereal,+ pretty,+ parsec,+ containers,+ bytestring,+ random,+ network,+ cml,+ HTTP,+ HsOpenSSL,+ time,+ random-shuffle,+ directory++ ghc-options: -threaded -fwarn-unused-imports+ if flag(debug)+ cpp-options: "-DDEBUG"+ else+ cpp-options: "-DNDEBUG"++source-repository head+ type: git+ location: git://github.com/jlouis/haskell-torrent.git+ branch: master+++
+ LICENSE view
@@ -0,0 +1,25 @@+Copyright (c) 2009, Jesper Louis Andersen+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are+met:++* Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++* Redistributions in binary form must reproduce the above copyright+ notice, this list of conditions and the following disclaimer in the+ documentation and/or other materials provided with the distribution.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,92 @@+Haskell Torrent - a haskell bittorrent client.+==========++Introduction+----------++This is a Haskell bittorrent client. I am the introduction document+and I need to be written by some generous soul!++Installation+------------++Here is what I do to install haskell torrrent locally on my machine:++ cabal install --prefix=$HOME --user++Since we are using the magnificient cabal, this is enough to install haskell torrent in our $HOME/bin directory.++Usage+-----------------++Haskell torrent can currently only do one very simple thing. If you call it with++ HaskellTorrent foo.torrent++then it will begin downloading the file in foo.torrent to the current directory via the Bittorrent protocol. *Note:* Currently we have no support for multifile torrents.++Protocol support+----------------++Currently haskell-torrent supports the following BEPs (See the+[BEP Process](http://www.bittorrent.org/beps/bep_0000.html) document for an+explanation of these)++ - 004, 020,++Haskell-torrent is not supporting these BEPs, but strives to do so one day:++ - 003, 005, 006, 007, 010, 012, 015, 009, 023, 018, 021, 022, 024, 026, 027,+ 028, 029, 030, 031, 032++Haskell-torrent will probably never support these BEPs:++ - 016, 017, 019++Source code Hierarchy+---------------------++ - **Data**: Data structures.+ - **Queue**: Functional queues. Standard variant with two lists.++ - **Process**: Process definitions for the different processes comprising Haskell Torrent+ - **ChokeMgr**: Manages choking and unchoking of peers, based upon the current speed of the peer+ and its current state. Global for multiple torrents.+ - **Console**: Simple console process. Only responds to 'quit' at the moment.+ - **FS**: Process managing the file system.+ - **Listen**: Not used at the moment. Step towards listening sockets.+ - **Peer**: Several process definitions for handling peers. Two for sending, one for receiving+ and one for controlling the peer and handle the state.+ - **PeerMgr**: Management of a set of peers for a single torrent.+ - **PieceMgr**: Keeps track of what pieces have been downloaded and what are missing. Also hands+ out blocks for downloading to the peers.+ - **Status**: Keeps track of uploaded/downloaded/left bytes for a single torrent. Could be globalized.+ - **Timer**: Timer events.+ - **Tracker**: Communication with the tracker.++ - **Protocol**: Modules for interacting with the various bittorrent protocols.+ - **BCode**: The bittorrent BCode coding. Used by several protocols.+ - **Wire**: The protocol used for communication between peers.++ - **Top Level**:+ - **Digest**: SHA1 digests as used in the bittorrent protocol.+ - **FS**: Low level Filesystem code. Interacts with files.+ - **HaskellTorrent**: Main entry point to the code. Sets up processes.+ - **Logging**: Logging interface.+ - **LoggingTypes**: Types and instances used by the Logging framework.+ - **PeerTypes**: Types used by peers.+ - **Process**: Code for Erlang-inspired processes.+ - **RateCalc**: Rate calculations for a network socket. We use this to keep track of the+ current speed of a peer in one direction.+ - **Supervisor**: Erlang-inspired Supervisor processes.+ - **Torrent**: Various helpers and types for Torrents.+ - **Version.hs.in**: Generates **Version.hs** via the configure script.++Known bugs+----------++ - I have seen the system violating an assertion in the Piece Manager. I think I got it,+ nailed, but it may show up again.+++
+ Setup.lhs view
@@ -0,0 +1,7 @@+#!/usr/bin/env runhaskell++In principle, we could do with a lot less than autoconfUserhooks, but simpleUserHooks+is not running 'configure'.++>import Distribution.Simple+>main = defaultMainWithHooks autoconfUserHooks
+ configure view
@@ -0,0 +1,21 @@+#!/bin/sh+#+# Simple configuration script.++## Git version+GITHEAD="0.0 Hackage"+if test -d "${GIT_DIR:-.git}" ; then+ GITHEAD=`git describe 2>/dev/null`++ if test -z ${GITHEAD} ; then+ GITHEAD=`git rev-parse HEAD`+ fi++ if test -n "`git diff-index -m --name-only HEAD`" ; then+ GITHEAD="${GITHEAD}-dirty"+ fi+fi++sed -e "s/@GITHEAD@/$GITHEAD/g" src/Version.hs.in > src/Version.hs++
+ src/Data/Queue.hs view
@@ -0,0 +1,42 @@+-- | Simple Functional queues based on a double list. This usually achieves good amortized bounds+module Data.Queue (+ -- * Types+ Queue,+ -- * Functions+ empty,+ isEmpty,+ push,+ pop,+ Data.Queue.filter)+where++import qualified Data.List as Lst++data Queue a = Queue [a] [a]++-- | The definition of an empty Queue+empty :: Queue a+empty = Queue [] []++-- | Returns True on an empty Queue, and False otherwise.+isEmpty :: Queue a -> Bool+isEmpty (Queue [] []) = True+isEmpty _ = False++-- | Pushes a new element to the tail of the list.+-- Operates in constant time.+push :: a -> Queue a -> Queue a+push e (Queue front back) = Queue front (e : back)+++-- | Pops the top most element off the queue.+-- Operates in amortized constant time+pop :: Queue a -> Maybe (a, Queue a)+pop (Queue [] []) = Nothing+pop (Queue (e : es) back) = Just (e, Queue es back)+pop (Queue [] back) = pop (Queue (reverse back) [])++-- | Generates a new Queue only containing elements for which+-- p returns true.+filter :: (a -> Bool) -> Queue a -> Queue a+filter p (Queue front back) = Queue (Lst.filter p front) (Lst.filter p back)
+ src/Digest.hs view
@@ -0,0 +1,32 @@+-- | Simple abstraction for message digests+module Digest+ ( Digest+ , digest+ )+where++import Control.Concurrent+import Control.Monad++import Data.Maybe++import qualified Data.ByteString.Lazy as L+import OpenSSL+import qualified OpenSSL.EVP.Digest as D++-- Consider newtyping this+type Digest = String++sha1Digest :: IO D.Digest+sha1Digest = do dig <- D.getDigestByName "SHA1"+ case dig of+ Nothing -> fail "No such digest, SHA1"+ Just d -> return d++digest :: L.ByteString -> IO Digest+digest bs =+ runInBoundThread $ -- I don't think this is needed strictly+ withOpenSSL $+ do sha1 <- sha1Digest+ return $ D.digestLBS sha1 bs+
+ src/FS.hs view
@@ -0,0 +1,226 @@+-- Haskell Torrent+-- Copyright (c) 2009, Jesper Louis Andersen,+-- All rights reserved.+--+-- Redistribution and use in source and binary forms, with or without+-- modification, are permitted provided that the following conditions are+-- met:+--+-- * Redistributions of source code must retain the above copyright+-- notice, this list of conditions and the following disclaimer.+-- * Redistributions in binary form must reproduce the above copyright+-- notice, this list of conditions and the following disclaimer in the+-- documentation and/or other materials provided with the distribution.+--+-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS+-- IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,+-- THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR+-- PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR+-- CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,+-- EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,+-- PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR+-- PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF+-- LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING+-- NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS+-- SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.++-- | Filesystem routines. These are used for working with and+-- manipulating files in the filesystem.+module FS (PieceInfo(..),+ PieceMap,+ Handles,+ readPiece,+ readBlock,+ writeBlock,+ mkPieceMap,+ checkFile,+ checkPiece,+ openAndCheckFile,+ canSeed)+where++import Control.Monad.State++import qualified Data.ByteString.Char8 as B+import qualified Data.ByteString.Lazy as L+import qualified Data.Map as M+import Data.Maybe+import System.IO+import System.Directory (createDirectoryIfMissing)+import Data.List (intercalate)++import Protocol.BCode as BCode+import qualified Digest as D+import Torrent++-- | For multi-file torrents we've got to maintain multiple file+-- handles. The data structure may as well be a Map Range Handle,+-- but that's detailto only @projectHandles@. More importantly,+-- functions operating on the files must be aware that a+-- piece/block can span multiple files.+--+-- FIXME: Replace this with a handle cache later. Many peers & many+-- tiny files will make us overstep the fd limit (usually+-- 1024).+newtype Handles = Handles [(Handle, Integer)] -- ^[(fileHandle, fileLength)]++projectHandles :: Handles+ -> Integer -- ^Torrent offset+ -> Integer -- ^Torrent size+ -> [(Handle -- ^File handle+ ,Integer -- ^File chunk offset+ ,Integer -- ^File chunk size+ )]+{-+projectHandles handles offset size = let r = projectHandles' handles offset size+ in trace ("projectHandles " +++ show handles ++ " " +++ show offset ++ " " +++ show size ++ " = " +++ show r+ ) $+ r+-}+projectHandles (Handles handles@((h1, length1):handles')) offset size+ | size <= 0 =+ fail "FS: Should have already stopped projection"+ | null handles =+ fail "FS: Attempt to read beyond torrent length"+ | offset >= length1 =+ projectHandles (Handles handles') (offset - length1) size+ | otherwise =+ let size1 = length1 - offset -- ^How much of h1 to take?+ in if size1 >= size+ then [(h1, offset, size)]+ else (h1, offset, size1) :+ projectHandles (Handles handles') 0 (size - size1)++pInfoLookup :: PieceNum -> PieceMap -> IO PieceInfo+pInfoLookup pn mp = case M.lookup pn mp of+ Nothing -> fail "FS: Error lookup in PieceMap"+ Just i -> return i++-- | FIXME: minor code duplication with @readBlock@+readPiece :: PieceNum -> Handles -> PieceMap -> IO L.ByteString+readPiece pn handles mp =+ do pInfo <- pInfoLookup pn mp+ bs <- L.concat `fmap`+ forM (projectHandles handles (offset pInfo) (len pInfo))+ (\(h, offset, size) ->+ do hSeek h AbsoluteSeek offset+ L.hGet h (fromInteger size)+ )+ if L.length bs == (fromInteger . len $ pInfo)+ then return bs+ else fail "FS: Wrong number of bytes read"++-- | FIXME: concatenating strict ByteStrings may turn out+-- expensive. Returning lazy ones may be more appropriate.+readBlock :: PieceNum -> Block -> Handles -> PieceMap -> IO B.ByteString+readBlock pn blk handles mp =+ do pInfo <- pInfoLookup pn mp+ B.concat `fmap`+ forM (projectHandles handles (offset pInfo + (fromIntegral $ blockOffset blk))+ (fromIntegral $ blockSize blk))+ (\(h, offset, size) ->+ do hSeek h AbsoluteSeek offset+ B.hGet h $ fromInteger size+ )++-- | The call @writeBlock h n blk pm blkData@ will write the contents of @blkData@+-- to the file pointed to by handle at the correct position in the file. If the+-- block is of a wrong length, the call will fail.+writeBlock :: Handles -> PieceNum -> Block -> PieceMap -> B.ByteString -> IO ()+writeBlock handles n blk pm blkData =+ do when lenFail $ fail "Writing block of wrong length"+ pInfo <- pInfoLookup n pm+ foldM_ (\blkData (h, offset, size) ->+ do let size' = fromInteger size+ hSeek h AbsoluteSeek offset+ B.hPut h $ B.take size' blkData+ hFlush h+ return $ B.drop size' blkData+ ) blkData (projectHandles handles (position pInfo) (fromIntegral $ B.length blkData))+ return ()+ where+ position :: PieceInfo -> Integer+ position pinfo = (offset pinfo) + fromIntegral (blockOffset blk)+ lenFail = B.length blkData /= blockSize blk++-- | The @checkPiece h inf@ checks the file system for correctness of a given piece, namely if+-- the piece described by @inf@ is correct inside the file pointed to by @h@.+checkPiece :: PieceInfo -> Handles -> IO Bool+checkPiece inf handles = do+ bs <- L.concat `fmap`+ forM (projectHandles handles (offset inf) (fromInteger $ len inf))+ (\(h, offset, size) ->+ do hSeek h AbsoluteSeek offset+ L.hGet h (fromInteger size)+ )+ dgs <- liftIO $ D.digest bs+ return (dgs == digest inf)++-- | Create a MissingMap from a file handle and a piecemap. The system will read each part of+-- the file and then check it against the digest. It will create a map of what we are missing+-- in the file as a missing map. We could alternatively choose a list of pieces missing rather+-- then creating the data structure here. This is perhaps better in the long run.+checkFile :: Handles -> PieceMap -> IO PiecesDoneMap+checkFile handles pm = do l <- mapM checkP pieces+ return $ M.fromList l+ where pieces = M.toAscList pm+ checkP :: (PieceNum, PieceInfo) -> IO (PieceNum, Bool)+ checkP (pn, pInfo) = do b <- checkPiece pInfo handles+ return (pn, b)++-- | Extract the PieceMap from a bcoded structure+-- Needs some more defense in the long run.+mkPieceMap :: BCode -> Maybe PieceMap+mkPieceMap bc = fetchData+ where fetchData = do pLen <- infoPieceLength bc+ pieceData <- infoPieces bc+ tLen <- infoLength bc+ let pm = M.fromList . zip [0..] . extract pLen tLen 0 $ pieceData+ when ( tLen /= (sum $ map len $ M.elems pm) )+ (error "PieceMap construction size assertion failed")+ return pm+ extract :: Integer -> Integer -> Integer -> [B.ByteString] -> [PieceInfo]+ extract _ 0 _ [] = []+ extract plen tlen offst (p : ps) | tlen < plen = PieceInfo { offset = offst,+ len = tlen,+ digest = B.unpack p } : extract plen 0 (offst + plen) ps+ | otherwise = inf : extract plen (tlen - plen) (offst + plen) ps+ where inf = PieceInfo { offset = offst,+ len = plen,+ digest = B.unpack p }+ extract _ _ _ _ = error "mkPieceMap: the impossible happened!"++-- | Predicate function. True if nothing is missing from the map.+canSeed :: PiecesDoneMap -> Bool+canSeed = M.fold (&&) True++-- | Process a BCoded torrent file. Create directories, open the files+-- in question, check it and return Handles plus a haveMap for the+-- file+openAndCheckFile :: BCode -> IO (Handles, PiecesDoneMap, PieceMap)+openAndCheckFile bc =+ do+ handles <- Handles `fmap`+ forM files+ (\(path, length) ->+ do let dir = joinPath $ init path+ when (dir /= "") $+ createDirectoryIfMissing True dir+ let fpath = joinPath path+ h <- openBinaryFile fpath ReadWriteMode+ return (h, length)+ )+ have <- checkFile handles pieceMap+ return (handles, have, pieceMap)+ where Just files = BCode.infoFiles bc+ Just pieceMap = mkPieceMap bc+ joinPath = intercalate "/"+++++
+ src/HaskellTorrent.hs view
@@ -0,0 +1,85 @@+module Main (main)+where++import Control.Concurrent.CML+import Control.Monad++import qualified Data.ByteString as B++import System.Environment+import System.Random+++import qualified Protocol.BCode as BCode+import qualified Process.Console as Console+import qualified Process.FS as FSP+import qualified Process.PeerMgr as PeerMgr+import qualified Process.PieceMgr as PieceMgr (start, createPieceDb)+import qualified Process.ChokeMgr as ChokeMgr (start)+import qualified Process.Status as Status+import qualified Process.Tracker as Tracker+import Logging+import FS+import Supervisor+import Torrent+import Version++main :: IO ()+main = getArgs >>= run+++run :: [String] -> IO ()+run args = do+ putStrLn $ "This is Haskell-torrent version " ++ version+ case args of+ [] -> putStrLn "*** Usage: haskellTorrent <file.torrent>"+ (name:_) -> download name++download :: String -> IO ()+download name = do+ torrent <- B.readFile name+ let bcoded = BCode.decode torrent+ case bcoded of+ Left pe -> print pe+ Right bc ->+ do print bc+ (handles, haveMap, pieceMap) <- openAndCheckFile bc+ logC <- channel+ Logging.startLogger logC+ -- setup channels+ trackerC <- channel+ statusC <- channel+ waitC <- channel+ pieceMgrC <- channel+ supC <- channel+ fspC <- channel+ statInC <- channel+ pmC <- channel+ chokeC <- channel+ chokeInfoC <- channel+ putStrLn "Created channels"+ -- setup StdGen and Peer data+ gen <- getStdGen+ ti <- mkTorrentInfo bc+ let pid = mkPeerId gen+ left = bytesLeft haveMap pieceMap+ clientState = determineState haveMap+ -- Create main supervisor process+ allForOne "MainSup"+ [ Worker $ Console.start logC waitC+ , Worker $ FSP.start handles logC pieceMap fspC+ , Worker $ PeerMgr.start pmC pid (infoHash ti)+ pieceMap pieceMgrC fspC logC chokeC statInC (pieceCount ti)+ , Worker $ PieceMgr.start logC pieceMgrC fspC chokeInfoC statInC+ (PieceMgr.createPieceDb haveMap pieceMap)+ , Worker $ Status.start logC left clientState statusC statInC trackerC+ , Worker $ Tracker.start ti pid defaultPort logC statusC statInC+ trackerC pmC+ , Worker $ ChokeMgr.start logC chokeC chokeInfoC 100 -- 100 is upload rate in KB+ (case clientState of+ Seeding -> True+ Leeching -> False)+ ] logC supC+ sync $ transmit trackerC Status.Start+ sync $ receive waitC (const True)+ return ()
+ src/Logging.hs view
@@ -0,0 +1,40 @@+-- | Logging primitives+{-# LANGUAGE TypeSynonymInstances #-}+module Logging (+ -- * Classes+ Logging(..)+ -- * Types+ , LogChannel+ , LogPriority(..)+ -- * Spawning+ , startLogger+ -- * Interface (deprecated)+ , logMsg+ , logMsg'+ )+where++import Control.Concurrent+import Control.Concurrent.CML+import Control.Monad.Reader++import LoggingTypes+import Prelude hiding (log)++import Process++startLogger :: LogChannel -> IO ThreadId+startLogger logC =+ spawnP logC () (forever lp)+ where+ lp = do m <- syncP =<< logEv+ liftIO $ print m+ logEv = recvP logC (const True)++-- | Log a message to a channel+logMsg :: LogChannel -> String -> IO ()+logMsg c m = logMsg' c "Unknown" Info m++-- | Log a message to a channel with a priority+logMsg' :: LogChannel -> String -> LogPriority -> String -> IO ()+logMsg' c name pri = sync . transmit c . Mes pri name
+ src/LoggingTypes.hs view
@@ -0,0 +1,36 @@+{-# LANGUAGE TypeSynonymInstances #-}+module LoggingTypes+ ( Logging(..)+ , LogPriority(..)+ , LogMsg(..)+ , LogChannel+ )+where++import Control.Concurrent.CML+++data LogPriority = Debug -- ^ Fine grained debug info+ | Warn -- ^ Potentially harmful situations+ | Info -- ^ Informational messages, progress reports+ | Error -- ^ Errors that are continuable+ | Fatal -- ^ Severe errors. Will probably make the application abort+ | None -- ^ No logging at all+ deriving (Show, Eq, Ord)+--+-- | The class of types where we have a logger inside them somewhere+class Logging a where+ -- | Returns a channel for logging and an Identifying string to use+ getLogger :: a -> (String, LogChannel)++instance Logging LogChannel where+ getLogger ch = ("Unknown", ch)+++-- TODO: Consider generalizing this to any member of Show+data LogMsg = Mes LogPriority String String++instance Show LogMsg where+ show (Mes pri name str) = show name ++ "(" ++ show pri ++ "):\t" ++ str++type LogChannel = Channel LogMsg
+ src/PeerTypes.hs view
@@ -0,0 +1,30 @@+module PeerTypes+where++import Control.Concurrent+import Control.Concurrent.CML++import Data.Time.Clock++import Network+import Torrent++data Peer = Peer { peerHost :: HostName,+ peerPort :: PortID }++data PeerMessage = ChokePeer+ | UnchokePeer+ | PeerStats UTCTime (Channel (Double, Double, Bool)) -- Up/Down/Interested+ | PieceCompleted PieceNum+ | CancelBlock PieceNum Block++type PeerChannel = Channel PeerMessage+++data MgrMessage = Connect ThreadId (Channel PeerMessage)+ | Disconnect ThreadId++type MgrChannel = Channel MgrMessage++-- | A Channel type we use for transferring the amount of data we transferred+type BandwidthChannel = Channel Integer
+ src/Process.hs view
@@ -0,0 +1,211 @@+-- | Core Process code+{-# LANGUAGE ExistentialQuantification, FlexibleInstances,+ GeneralizedNewtypeDeriving,+ MultiParamTypeClasses, CPP #-}+-- required for deriving Typeable+{-# OPTIONS_GHC -fglasgow-exts #-}++module Process (+ -- * Types+ Process+ -- * Interface+ , runP+ , spawnP+ , catchP+ , cleanupP+ , foreverP+ , syncP+ , chooseP+ , sendP+ , sendPC+ , recvP+ , recvPC+ , recvWrapPC+ , wrapP+ , stopP+ , ignoreProcessBlock -- This ought to be renamed+ -- * Log Interface+ , logInfo+ , logDebug+ , logWarn+ , logFatal+ , logError+ )+where++import Data.Monoid++import Control.Concurrent+import Control.Concurrent.CML+import Control.Exception++import Control.Monad.Reader+import Control.Monad.State++import Data.Typeable++import LoggingTypes+import Prelude hiding (catch, log)++import System.IO++-- import Supervisor+++-- | A @Process a b c@ is the type of processes with access to configuration data @a@, state @b@+-- returning values of type @c@. Usually, the read-only data are configuration parameters and+-- channels, and the state the internal process state. It is implemented by means of a transformer+-- stack on top of IO.+newtype Process a b c = Process (ReaderT a (StateT b IO) c)+#ifndef __HADDOCK__+ deriving (Functor, Monad, MonadIO, MonadState b, MonadReader a, Typeable)+#endif++data StopException = StopException+ deriving (Show, Typeable)++instance Exception StopException++stopP :: Process a b c+stopP = throw StopException++-- | Run the process monad given a configuation of type @a@ and a initial state of type @b@+runP :: a -> b -> Process a b c -> IO (c, b)+runP c st (Process p) = runStateT (runReaderT p c) st++-- | Spawn and run a process monad+spawnP :: a -> b -> Process a b () -> IO ThreadId+spawnP c st p = spawn proc+ where proc = do runP c st p+ return ()++-- | Run the process monad for its side effect, with a stopHandler if exceptions+-- are raised in the process+catchP :: Logging a => Process a b () -> Process a b () -> Process a b ()+catchP proc stopH = cleanupP proc stopH (return ())++-- | Run the process monad for its side effect. @cleanupP p sh ch@ describes to+-- run @p@. If @p@ dies by a kill from a supervisor, run @ch@. Otherwise it runs+-- @ch >> sh@ on death.+cleanupP :: Logging a => Process a b () -> Process a b () -> Process a b () -> Process a b ()+cleanupP proc stopH cleanupH = do+ st <- get+ c <- ask+ (a, s') <- liftIO $ runP c st proc `catches`+ [ Handler (\ThreadKilled -> do+ runP c st ( do logInfo $ "Process Terminated by Supervisor"+ cleanupH ))+ , Handler (\StopException -> + runP c st (do logInfo $ "Process Terminating gracefully"+ cleanupH >> stopH)) -- This one is ok+ , Handler (\(ex :: SomeException) ->+ runP c st (do logFatal $ "Process exiting due to ex: " ++ show ex+ cleanupH >> stopH))+ ]+ put s'+ return a++-- | Run a process forever in a loop+foreverP :: Process a b c -> Process a b c+foreverP p = p >> foreverP p++syncP :: Event (c, b) -> Process a b c+syncP ev = do (a, s) <- liftIO $ sync ev+ put s+ return a++sendP :: Channel c -> c -> Process a b (Event ((), b))+sendP ch v = do+ s <- get+ return $ (wrap (transmit ch v)+ (\() -> return ((), s)))++sendPC :: (a -> Channel c) -> c -> Process a b (Event ((), b))+sendPC sel v = asks sel >>= flip sendP v++recvP :: Channel c -> (c -> Bool) -> Process a b (Event (c, b))+recvP ch pred = do+ s <- get+ return (wrap (receive ch pred)+ (\v -> return (v, s)))++recvPC :: (a -> Channel c) -> Process a b (Event (c, b))+recvPC sel = asks sel >>= flip recvP (const True)++wrapP :: Event (c, b) -> (c -> Process a b y) -> Process a b (Event (y, b))+wrapP ev p = do+ c <- ask+ return $ wrap ev (\(v, s) -> runP c s (p v))++-- Convenience function+recvWrapPC :: (a -> Channel c) -> (c -> Process a b y) -> Process a b (Event (y, b))+recvWrapPC sel p = do+ ev <- recvPC sel+ wrapP ev p++chooseP :: [Process a b (Event (c, b))] -> Process a b (Event (c, b))+chooseP events = (sequence events) >>= (return . choose)++-- VERSION SPECIFIC PROCESS ORIENTED FUNCTIONS++-- | @ignoreProcessBlock err thnk@ runs a process action, ignoring blocks on dead+-- MVars. If the MVar is blocked, return the default value @err@.+ignoreProcessBlock :: c -> Process a b c -> Process a b c+ignoreProcessBlock err thnk = do+ st <- get+ c <- ask+ (a, s') <- liftIO $ runP c st thnk `catches`+ -- Peer dead, ignore+#if (__GLASGOW_HASKELL__ == 610)+ [ Handler (\BlockedOnDeadMVar -> return (err, st)) ]+#elif (__GLASGOW_HASKELL__ == 612)+ [ Handler (\BlockedIndefinitelyOnMVar -> return (err, st)) ]+#else+#error Unknown GHC version+#endif+ put s'+ return a++------ LOGGING++-- | If a process has access to a logging channel, it is able to log messages to the world+log :: Logging a => LogPriority -> String -> Process a b ()+log prio msg = do+ (name, logC) <- asks getLogger+ when (prio >= logLevel name)+ (liftIO $ logMsg' logC name prio msg)+ where logMsg' c name pri = sync . transmit c . Mes pri name++logInfo, logDebug, logFatal, logWarn, logError :: Logging a => String -> Process a b ()+logInfo = log Info+logDebug = log Debug+logFatal = log Fatal+logWarn = log Warn+logError = log Error++-- Logging filters+type LogFilter = String -> LogPriority++matchP :: String -> LogPriority -> LogFilter+matchP process prio = \s -> if s == process then prio else None++matchAny :: LogPriority -> LogFilter+matchAny prio = const prio++matchNone :: LogFilter+matchNone = const None++instance Monoid LogPriority where+ mempty = None+ mappend None g = g+ mappend f g = f++-- | The level by which we log+logLevel :: LogFilter+#ifdef DEBUG+logLevel = mconcat [matchP "TrackerP" Debug,+ matchAny Info]+#else+logLevel = matchAny Info+#endif+
+ src/Process/ChokeMgr.hs view
@@ -0,0 +1,358 @@+module Process.ChokeMgr (+ -- * Types, Channels+ ChokeMgrChannel+ , ChokeMgrMsg(..)+ -- * Interface+ , start+ )+where++import Data.Time.Clock+import Data.List+import qualified Data.Map as M+import qualified Data.Set as S+import Data.Traversable as T++import Control.Concurrent+import Control.Concurrent.CML+import Control.Monad.Reader+import Control.Monad.State+++import Prelude hiding (catch, log)++import System.Random++import PeerTypes+import Process.PieceMgr hiding (start)+import Process+import Logging+import Supervisor+import Torrent hiding (infoHash)+import Process.Timer as Timer++-- DATA STRUCTURES+----------------------------------------------------------------------++data ChokeMgrMsg = Tick+ | RemovePeer PeerPid+ | AddPeer PeerPid PeerChannel+type ChokeMgrChannel = Channel ChokeMgrMsg++data CF = CF { logCh :: LogChannel+ , mgrCh :: ChokeMgrChannel+ , infoCh :: ChokeInfoChannel+ }++instance Logging CF where+ getLogger cf = ("ChokeMgrP", logCh cf)++type ChokeMgrProcess a = Process CF PeerDB a++-- INTERFACE+----------------------------------------------------------------------++start :: LogChannel -> ChokeMgrChannel -> ChokeInfoChannel -> Int -> Bool -> SupervisorChan+ -> IO ThreadId+start logC ch infoC ur weSeed supC = do+ Timer.register 10 Tick ch+ spawnP (CF logC ch infoC) (initPeerDB $ calcUploadSlots ur Nothing)+ (catchP (forever pgm)+ (defaultStopHandler supC))+ where+ initPeerDB slots = PeerDB 2 weSeed slots M.empty []+ pgm = do chooseP [mgrEvent, infoEvent] >>= syncP+ mgrEvent =+ recvWrapPC mgrCh+ (\msg -> case msg of+ Tick -> tick+ RemovePeer t -> removePeer t+ AddPeer t pCh -> do+ logDebug $ "Adding peer " ++ show t+ weSeed <- gets seeding+ addPeer pCh weSeed t)+ infoEvent =+ recvWrapPC infoCh+ (\m -> case m of+ BlockComplete pn blk -> informBlockComplete pn blk+ PieceDone pn -> informDone pn+ TorrentComplete -> do+ modify (\s -> s { seeding = True+ , peerMap =+ M.map (\pi -> pi { pAreSeeding = True })+ $ peerMap s}))+ tick = do logDebug "Ticked"+ ch <- asks mgrCh+ Timer.register 10 Tick ch+ updateDB+ runRechokeRound+ removePeer tid = do logDebug $ "Removing peer " ++ show tid+ modify (\db -> db { peerMap = M.delete tid (peerMap db),+ peerChain = (peerChain db) \\ [tid] })++-- INTERNAL FUNCTIONS+----------------------------------------------------------------------++type PeerPid = ThreadId -- For now, should probably change+++-- | The PeerDB is the database we keep over peers. It maps all the information necessary to determine+-- which peers are interesting to keep uploading to and which are slow. It also keeps track of how+-- far we are in the process of wandering the optimistic unchoke chain.+data PeerDB = PeerDB+ { chokeRound :: Int -- ^ Counted down by one from 2. If 0 then we should+ -- advance the peer chain.+ , seeding :: Bool -- ^ True if we are seeding the torrent.+ -- In a multi-torrent world, this has to change.+ , uploadSlots :: Int -- ^ Current number of upload slots+ , peerMap :: PeerMap -- ^ Map of peers+ , peerChain :: [PeerPid] -- ^ The order in which peers are optimistically unchoked+ }++-- | The PeerInfo structure maps, for each peer pid, its accompanying informative data for the PeerDB+data PeerInfo = PeerInfo+ { pChokingUs :: Bool -- ^ True if the peer is choking us+ , pDownRate :: Double -- ^ The rate of the peer in question, bytes downloaded in last window+ , pUpRate :: Double -- ^ The rate of the peer in question, bytes uploaded in last window+ , pChannel :: PeerChannel -- ^ The channel on which to communicate with the peer+ , pInterestedInUs :: Bool -- ^ Reflection from Peer DB+ , pAreSeeding :: Bool -- ^ True if this peer is connected on a torrent we seed+ , pIsASeeder :: Bool -- ^ True if the peer is a seeder+ }++type PeerMap = M.Map PeerPid PeerInfo++-- | Auxilliary data structure. Used in the rechoking process.+type RechokeData = (PeerPid, PeerInfo)++-- | Comparison with inverse ordering+compareInv :: Ord a => a -> a -> Ordering+compareInv x y =+ case compare x y of+ LT -> GT+ EQ -> EQ+ GT -> LT++comparingWith :: Ord a => (a -> a -> Ordering) -> (b -> a) -> b -> b -> Ordering+comparingWith comp project x y =+ comp (project x) (project y)++-- | Leechers are sorted by their current download rate. We want to keep fast peers around.+sortLeech :: [RechokeData] -> [RechokeData]+sortLeech = sortBy (comparingWith compareInv $ pDownRate . snd)++-- | Seeders are sorted by their current upload rate.+sortSeeds :: [RechokeData] -> [RechokeData]+sortSeeds = sortBy (comparingWith compareInv $ pUpRate . snd)++-- | Advance the peer chain to the next peer eligible for optimistic+-- unchoking. That is, skip peers which are not interested in our pieces+-- and peers which are not choking us. The former we can't send any data to,+-- so we can't get better speeds at them. The latter are already sending us data,+-- so we know how good they are as peers.+advancePeerChain :: ChokeMgrProcess [PeerPid]+advancePeerChain = do+ peers <- gets peerChain+ mp <- gets peerMap+ lPeers <- T.mapM (lookupPeer mp) peers+ let (front, back) = break (\(_, p) -> pInterestedInUs p && pChokingUs p) lPeers+ return $ map fst $ back ++ front+ where+ lookupPeer mp peer = case M.lookup peer mp of+ Nothing -> fail "Could not look up peer in map"+ Just p -> return (peer, p)++-- | Add a peer to the Peer Database+addPeer :: PeerChannel -> Bool -> PeerPid -> ChokeMgrProcess ()+addPeer pCh weSeeding tid = do+ addPeerChain tid+ modify (\db -> db { peerMap = M.insert tid initialPeerInfo (peerMap db)})+ where+ initialPeerInfo = PeerInfo { pChokingUs = True+ , pDownRate = 0.0+ , pUpRate = 0.0+ , pChannel = pCh+ , pInterestedInUs = False+ , pAreSeeding = weSeeding+ , pIsASeeder = False -- May get updated quickly+ }++-- | Insert a Peer randomly into the Peer chain. Threads the random number generator+-- through.+addPeerChain :: PeerPid -> ChokeMgrProcess ()+addPeerChain pid = do+ ls <- gets peerChain+ pt <- liftIO $ getStdRandom (\gen -> randomR (0, length ls - 1) gen)+ let (front, back) = splitAt pt ls+ modify (\db -> db { peerChain = (front ++ pid : back) })++-- | Calculate the amount of upload slots we have available. If the+-- number of slots is explicitly given, use that. Otherwise we+-- choose the slots based the current upload rate set. The faster+-- the rate, the more slots we allow.+calcUploadSlots :: Int -> Maybe Int -> Int+calcUploadSlots _ (Just n) = n+calcUploadSlots rate Nothing | rate <= 0 = 7 -- This is just a guess+ | rate < 9 = 2+ | rate < 15 = 3+ | rate < 42 = 4+ | otherwise = round . sqrt $ fromIntegral rate * 0.6++-- | The call @assignUploadSlots c ds ss@ will assume that we have @c@+-- slots for uploading at our disposal. The list @ds@ will be peers+-- that we would like to upload to among the torrents we are+-- currently downloading. The list @ss@ is the same thing but for+-- torrents that we seed. The function returns a pair @(kd,ks)@+-- where @kd@ is the number of downloader slots and @ks@ is the+-- number of seeder slots.+--+-- The function will move surplus slots around so all of them gets used.+assignUploadSlots :: Int -> [RechokeData] -> [RechokeData] -> (Int, Int)+assignUploadSlots slots downloaderPeers seederPeers =+ -- Shuffle surplus slots around so all gets used+ shuffleSeeders downloaderPeers seederPeers $ shuffleDownloaders+ downloaderPeers+ (downloaderSlots, seederSlots)+ where+ -- Calculate the slots available for the downloaders and seeders+ downloaderSlots = max 1 $ round $ fromIntegral slots * 0.7+ seederSlots = max 1 $ round $ fromIntegral slots * 0.3++ -- If there is a surplus of downloader slots, then assign them to+ -- the seeder slots+ shuffleDownloaders dPeers (dSlots, sSlots) =+ case max 0 (dSlots - length dPeers) of+ 0 -> (dSlots, sSlots)+ k -> (dSlots - k, sSlots + k)++ -- If there is a surplus of seeder slots, then assign these to+ -- the downloader slots. Limit the downloader slots to the number+ -- of downloaders, however+ shuffleSeeders dPeers sPeers (dSlots, sSlots) =+ case max 0 (sSlots - length sPeers) of+ 0 -> (dSlots, sSlots)+ k -> (min (dSlots + k) (length dPeers), sSlots - k)++-- | @selectPeers upSlots d s@ selects peers from a list of downloader peers @d@ and a list of seeder+-- peers @s@. The value of @upSlots@ defines the number of upload slots available+selectPeers :: Int -> [RechokeData] -> [RechokeData] -> ChokeMgrProcess (S.Set PeerPid)+selectPeers uploadSlots downPeers seedPeers = do+ let (nDownSlots, nSeedSlots) = assignUploadSlots uploadSlots downPeers seedPeers+ downPids = S.fromList $ map fst $ take nDownSlots $ sortLeech downPeers+ seedPids = S.fromList $ map fst $ take nSeedSlots $ sortSeeds seedPeers+ logDebug $ "Slots: " ++ show nDownSlots ++ " downloads, " ++ show nSeedSlots ++ " seeders"+ when (uploadSlots < nDownSlots + nSeedSlots)+ (fail "Wrong calculation of slots")+ logDebug $ "Electing peers - leechers: " ++ show downPids ++ "; seeders: " ++ show seedPids+ rm <- rateMap+ logDebug $ "Peer rates: " ++ rm+ return $ S.union downPids seedPids+ where+ rateMap :: ChokeMgrProcess String+ rateMap = do+ pm <- gets peerMap+ rts <- return $ map (\(pid, pi) ->+ show pid ++ " Up: " ++ show (pUpRate pi) ++ " Down: " ++ show (pDownRate pi))+ $ M.toList pm+ return $ show rts++-- | Send a message to the peer process at PeerChannel. May raise+-- exceptions if the peer is not running anymore.+msgPeer :: PeerMessage -> PeerChannel -> ChokeMgrProcess ()+msgPeer msg ch = syncP =<< sendP ch msg++-- | This function carries out the choking and unchoking of peers in a round.+performChokingUnchoking :: S.Set PeerPid -> [RechokeData] -> ChokeMgrProcess ()+performChokingUnchoking elected peers =+ do T.mapM (unchoke . snd) unchokers+ optChoke defaultOptimisticSlots chokers+ where+ -- Partition the peers based on they were selected or not+ (unchokers, chokers) = partition (\rd -> S.member (fst rd) elected) peers+ -- Choke and unchoke helpers.+ -- If we block on the sync, it means that the process in the other end must+ -- be dead. Thus we can just skip it. We will eventually receive this knowledge+ -- through another channel.+ unchoke pi = ignoreProcessBlock () $ unchokePeer (pChannel pi)+ choke pi = ignoreProcessBlock () $ chokePeer (pChannel pi)+ -- If we have k optimistic slots, @optChoke k peers@ will unchoke the first @k@ interested+ -- in us. The rest will either be unchoked if they are not interested (ensuring fast start+ -- should they become interested); or they will be choked to avoid TCP/IP congestion.+ optChoke _ [] = return ()+ optChoke 0 ((_, pi) : ps) = do if pInterestedInUs pi+ then chokePeer (pChannel pi)+ else unchokePeer (pChannel pi)+ optChoke 0 ps+ optChoke k ((_, pi) : ps) = if pInterestedInUs pi+ then unchokePeer (pChannel pi) >> optChoke (k-1) ps+ else unchokePeer (pChannel pi) >> optChoke k ps+ chokePeer = msgPeer ChokePeer+ unchokePeer = msgPeer UnchokePeer++-- | Function to split peers into those where we are seeding and those were we are leeching.+-- also prunes the list for peers which are not interesting.+-- TODO: Snubbed peers+splitSeedLeech :: [RechokeData] -> ([RechokeData], [RechokeData])+splitSeedLeech ps = partition (pAreSeeding . snd) $ filter picker ps+ where+ -- TODO: pIsASeeder is always false at the moment+ picker (_, pi) = not (pIsASeeder pi) && pInterestedInUs pi+++buildRechokeData :: ChokeMgrProcess [RechokeData]+buildRechokeData = do+ chain <- gets peerChain+ pm <- gets peerMap+ T.mapM (cPeer pm) chain+ where cPeer pm pid = case M.lookup pid pm of+ Nothing -> fail "buildRechokeData: Couldn't lookup pid"+ Just x -> return (pid, x)++rechoke :: ChokeMgrProcess ()+rechoke = do+ peers <- buildRechokeData+ us <- gets uploadSlots+ let (seed, down) = splitSeedLeech peers+ electedPeers <- selectPeers us down seed+ performChokingUnchoking electedPeers peers++-- | Traverse all peers and process them with a thunk.+traversePeers thnk = T.mapM thnk =<< gets peerMap++informDone :: PieceNum -> ChokeMgrProcess ()+informDone pn = traversePeers sendDone >> return ()+ where+ sendDone pi = ignoreProcessBlock ()+ $ (sendP (pChannel pi) $ PieceCompleted pn) >>= syncP++informBlockComplete :: PieceNum -> Block -> ChokeMgrProcess ()+informBlockComplete pn blk = traversePeers sendComp >> return ()+ where+ sendComp pi = ignoreProcessBlock ()+ $ (sendP (pChannel pi) $ CancelBlock pn blk) >>= syncP++updateDB :: ChokeMgrProcess ()+updateDB = do+ nmp <- traversePeers gatherRate+ modify (\db -> db { peerMap = nmp })+ where+ gatherRate pi = do+ ch <- liftIO channel+ t <- liftIO getCurrentTime+ ignoreProcessBlock pi (gather t ch pi)+ gather t ch pi = do+ (sendP (pChannel pi) $ PeerStats t ch) >>= syncP+ (uprt, downrt, interested) <- recvP ch (const True) >>= syncP+ return pi { pDownRate = downrt,+ pUpRate = uprt,+ pInterestedInUs = interested } -- TODO: Seeder state++runRechokeRound :: ChokeMgrProcess ()+runRechokeRound = do+ cRound <- gets chokeRound+ if (cRound == 0)+ then do nChain <- advancePeerChain+ modify (\db -> db { chokeRound = 2,+ peerChain = nChain })+ else modify (\db -> db { chokeRound = (chokeRound db) - 1 })+ rechoke
+ src/Process/Console.hs view
@@ -0,0 +1,81 @@+-- Haskell Torrent+-- Copyright (c) 2009, Jesper Louis Andersen,+-- All rights reserved.+--+-- Redistribution and use in source and binary forms, with or without+-- modification, are permitted provided that the following conditions are+-- met:+--+-- * Redistributions of source code must retain the above copyright+-- notice, this list of conditions and the following disclaimer.+-- * Redistributions in binary form must reproduce the above copyright+-- notice, this list of conditions and the following disclaimer in the+-- documentation and/or other materials provided with the distribution.+--+-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS+-- IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,+-- THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR+-- PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR+-- CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,+-- EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,+-- PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR+-- PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF+-- LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING+-- NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS+-- SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.++-- | The Console process has two main purposes. It is a telnet-like+-- interface with the user and it is our first simple logging device+-- for what happens inside the system.+{-# LANGUAGE ScopedTypeVariables #-}+module Process.Console+ ( start+ )+where++import Control.Concurrent+import Control.Concurrent.CML+import Control.Monad.Reader++import Prelude hiding (catch)+import Process++import Logging+import Supervisor++data Cmd = Quit -- Quit the program+ deriving (Eq, Show)++type CmdChannel = Channel Cmd++data CF = CF { cmdCh :: CmdChannel+ , logCh :: LogChannel }++instance Logging CF where+ getLogger cf = ("ConsoleP", logCh cf)++-- | Start the logging process and return a channel to it. Sending on this+-- Channel means writing stuff out on stdOut+start :: LogChannel -> Channel () -> SupervisorChan -> IO ThreadId+start logC waitC supC = do+ cmdC <- readerP logC -- We shouldn't be doing this in the long run+ spawnP (CF cmdC logC) () (catchP (forever lp) (defaultStopHandler supC))+ where+ lp = syncP =<< quitEvent+ quitEvent = do+ ch <- asks cmdCh+ ev <- recvP ch (==Quit)+ wrapP ev + (\_ -> syncP =<< sendP waitC ())+ ++readerP :: LogChannel -> IO CmdChannel+readerP logCh = do cmdCh <- channel+ spawn $ lp cmdCh+ return cmdCh+ where lp cmdCh = do c <- getLine+ case c of+ "quit" -> sync $ transmit cmdCh Quit+ cmd -> do logMsg' logCh "Console" Info $ "Unrecognized command: " ++ show cmd+ lp cmdCh+
+ src/Process/FS.hs view
@@ -0,0 +1,112 @@+-- Haskell Torrent+-- Copyright (c) 2009, Jesper Louis Andersen,+-- All rights reserved.+--+-- Redistribution and use in source and binary forms, with or without+-- modification, are permitted provided that the following conditions are+-- met:+--+-- * Redistributions of source code must retain the above copyright+-- notice, this list of conditions and the following disclaimer.+-- * Redistributions in binary form must reproduce the above copyright+-- notice, this list of conditions and the following disclaimer in the+-- documentation and/or other materials provided with the distribution.+--+-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS+-- IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,+-- THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR+-- PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR+-- CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,+-- EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,+-- PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR+-- PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF+-- LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING+-- NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS+-- SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.++-- | File system process. Acts as a maintainer for the filesystem in+-- question and can only do single-file torrents. It should be+-- fairly easy to add Multi-file torrents by altering this file and+-- the FS module.+{-# LANGUAGE ScopedTypeVariables #-}+module Process.FS+ ( FSPChannel+ , FSPMsg(..)+ , start+ )+where++import Control.Concurrent+import Control.Concurrent.CML+import Control.Monad.State+import Control.Monad.Reader++import System.IO+++import qualified Data.ByteString as B+import qualified Data.Map as M++import Logging+import Process+import Torrent+import qualified FS+import Supervisor++data FSPMsg = CheckPiece PieceNum (Channel (Maybe Bool))+ | WriteBlock PieceNum Block B.ByteString+ | ReadBlock PieceNum Block (Channel B.ByteString)++type FSPChannel = Channel FSPMsg++data CF = CF+ { fspCh :: FSPChannel -- ^ Channel on which to receive messages+ , logCh :: LogChannel -- ^ Channel for logging+ }++instance Logging CF where+ getLogger cf = ("FSP", logCh cf)++data ST = ST+ { fileHandles :: FS.Handles -- ^ The file we are working on+ , pieceMap :: FS.PieceMap -- ^ Map of where the pieces reside+ }+++-- INTERFACE+----------------------------------------------------------------------++start :: FS.Handles -> LogChannel -> FS.PieceMap -> FSPChannel -> SupervisorChan-> IO ThreadId+start handles logC pm fspC supC =+ spawnP (CF fspC logC) (ST handles pm) (catchP (forever lp) (defaultStopHandler supC))+ where+ lp = msgEvent >>= syncP+ msgEvent = do+ ev <- recvPC fspCh+ wrapP ev (\msg ->+ case msg of+ CheckPiece n ch -> do+ pm <- gets pieceMap+ case M.lookup n pm of+ Nothing -> sendP ch Nothing >>= syncP+ Just pi -> do r <- gets fileHandles >>= (liftIO . FS.checkPiece pi)+ sendP ch (Just r) >>= syncP+ ReadBlock n blk ch -> do+ logDebug $ "Reading block #" ++ show n+ ++ "(" ++ show (blockOffset blk) ++ ", " ++ show (blockSize blk) ++ ")"+ -- TODO: Protection, either here or in the Peer code+ h <- gets fileHandles+ bs <- gets pieceMap >>= (liftIO . FS.readBlock n blk h)+ sendP ch bs >>= syncP+ WriteBlock pn blk bs -> do+ -- TODO: Protection, either here or in the Peer code+ fh <- gets fileHandles+ pm <- gets pieceMap+ liftIO $ FS.writeBlock fh pn blk pm bs)++checkPiece :: FSPChannel -> PieceNum -> IO (Maybe Bool)+checkPiece fspC n = do+ ch <- channel+ sync . transmit fspC $ CheckPiece n ch+ sync $ receive ch (const True)+
+ src/Process/Listen.hs view
@@ -0,0 +1,34 @@+module ListenP+where++-- TODO: Create the OMBox here rather than taking it as a parameter+{-+import Control.Concurrent.CML++import Network++import ConsoleP+import PeerP+import Torrent+import FSP++++start :: PortID -> PeerId -> InfoHash -> FSPChannel -> LogChannel -> IO ()+start port pid ih fsC logC =+ do sock <- listenOn port+ spawn $ acceptor sock pid ih fsC logC+ return ()+++acceptor :: Socket -> PeerId -> InfoHash -> FSPChannel -> LogChannel -> IO ()+acceptor sock pid ih fsC logC =+ do (h, _, _) <- accept sock+ spawn $ acceptor sock pid ih fsC logC+ r <- PeerP.listenHandshake h pid ih fsC logC+ case r of+ Left err -> do logMsg logC $ "Incoming peer Accept error" ++ err+ return ()+ Right () -> return ()+-}+
+ src/Process/Peer.hs view
@@ -0,0 +1,460 @@+-- | Peer proceeses+{-# LANGUAGE ScopedTypeVariables #-}+module Process.Peer (+ -- * Types+ PeerMessage(..)+ -- * Interface+ , connect+ )+where++import Control.Concurrent+import Control.Concurrent.CML+import Control.Monad.State+import Control.Monad.Reader++import Prelude hiding (catch, log)++import Data.Bits+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as L+import qualified Data.Serialize.Get as G++import qualified Data.Map as M+import Data.Maybe++import Data.Set as S hiding (map)+import Data.Time.Clock+import Data.Word++import Network++import System.IO++import PeerTypes+import Process+import Logging+import Process.FS+import Process.PieceMgr+import qualified Data.Queue as Q+import RateCalc as RC+import Process.Status+import Supervisor+import Process.Timer as Timer+import Torrent+import Protocol.Wire++-- INTERFACE+----------------------------------------------------------------------+++type ConnectRecord = (HostName, PortID, PeerId, InfoHash, PieceMap)++connect :: ConnectRecord -> SupervisorChan -> PieceMgrChannel -> FSPChannel -> LogChannel+ -> StatusChan+ -> MgrChannel -> Int+ -> IO ThreadId+connect (host, port, pid, ih, pm) pool pieceMgrC fsC logC statC mgrC nPieces =+ spawn (connector >> return ())+ where connector =+ do logMsg logC $ "Connecting to " ++ show host ++ " (" ++ showPort port ++ ")"+ h <- connectTo host port+ logMsg logC "Connected, initiating handShake"+ r <- initiateHandshake logC h pid ih+ logMsg logC "Handshake run"+ case r of+ Left err -> do logMsg logC ("Peer handshake failure at host " ++ host+ ++ " with error " ++ err)+ return ()+ Right (_caps, _rpid) ->+ do logMsg logC "entering peerP loop code"+ supC <- channel -- TODO: Should be linked later on+ children <- peerChildren logC h mgrC pieceMgrC fsC statC pm nPieces+ sync $ transmit pool $ SpawnNew (Supervisor $ allForOne "PeerSup" children logC)+ return ()++-- INTERNAL FUNCTIONS+----------------------------------------------------------------------++data SPCF = SPCF { spLogCh :: LogChannel+ , spMsgCh :: Channel B.ByteString+ }++instance Logging SPCF where+ getLogger cf = ("SenderP", spLogCh cf)++-- | The raw sender process, it does nothing but send out what it syncs on.+senderP :: LogChannel -> Handle -> Channel B.ByteString -> SupervisorChan -> IO ThreadId+senderP logC h ch supC = spawnP (SPCF logC ch) h (catchP (foreverP pgm)+ (do t <- liftIO $ myThreadId+ syncP =<< (sendP supC $ IAmDying t)+ liftIO $ hClose h))+ where+ pgm :: Process SPCF Handle ()+ pgm = do+ c <- ask+ m <- syncP =<< recvPC spMsgCh+ h <- get+ liftIO $ do B.hPut h m+ hFlush h++-- | Messages we can send to the Send Queue+data SendQueueMessage = SendQCancel PieceNum Block -- ^ Peer requested that we cancel a piece+ | SendQMsg Message -- ^ We want to send the Message to the peer+ | SendOChoke -- ^ We want to choke the peer+ | SendQRequestPrune PieceNum Block -- ^ Prune SendQueue of this (pn, blk) pair++data SQCF = SQCF { sqLogC :: LogChannel+ , sqInCh :: Channel SendQueueMessage+ , sqOutCh :: Channel B.ByteString+ , bandwidthCh :: BandwidthChannel+ }++data SQST = SQST { outQueue :: Q.Queue Message+ , bytesTransferred :: Integer+ }++instance Logging SQCF where+ getLogger cf = ("SendQueueP", sqLogC cf)++-- | sendQueue Process, simple version.+-- TODO: Split into fast and slow.+sendQueueP :: LogChannel -> Channel SendQueueMessage -> Channel B.ByteString -> BandwidthChannel + -> SupervisorChan+ -> IO ThreadId+sendQueueP logC inC outC bandwC supC = spawnP (SQCF logC inC outC bandwC) (SQST Q.empty 0)+ (catchP (foreverP pgm)+ (defaultStopHandler supC))+ where+ pgm :: Process SQCF SQST ()+ pgm = do+ q <- gets outQueue+ l <- gets bytesTransferred+ case (Q.isEmpty q, l > 0) of+ (True, False) -> queueEvent >>= syncP+ (True, True ) -> chooseP [queueEvent, rateUpdateEvent] >>= syncP+ (False, False) -> chooseP [queueEvent, sendEvent] >>= syncP+ (False, True) -> chooseP [queueEvent, sendEvent, rateUpdateEvent] >>= syncP+ rateUpdateEvent = do+ l <- gets bytesTransferred+ ev <- sendPC bandwidthCh l+ wrapP ev (\() ->+ modify (\s -> s { bytesTransferred = 0 }))+ queueEvent = do+ ev <- recvPC sqInCh+ wrapP ev (\m -> case m of+ SendQMsg msg -> do logDebug "Queueing event for sending"+ modifyQ (Q.push msg)+ SendQCancel n blk -> modifyQ (Q.filter (filterPiece n (blockOffset blk)))+ SendOChoke -> do modifyQ (Q.filter filterAllPiece)+ modifyQ (Q.push Choke)+ SendQRequestPrune n blk ->+ modifyQ (Q.filter (filterRequest n blk)))+ modifyQ :: (Q.Queue Message -> Q.Queue Message) -> Process SQCF SQST ()+ modifyQ f = modify (\s -> s { outQueue = f (outQueue s) })+ sendEvent = do+ Just (e, r) <- gets (Q.pop . outQueue)+ let bs = encodePacket e+ tEvt <- sendPC sqOutCh bs+ wrapP tEvt (\() -> do logDebug "Dequeued event"+ modify (\s -> s { outQueue = r,+ bytesTransferred =+ bytesTransferred s + fromIntegral (B.length bs)}))+ filterAllPiece (Piece _ _ _) = True+ filterAllPiece _ = False+ filterPiece n off m =+ case m of Piece n off _ -> False+ _ -> True+ filterRequest n blk m =+ case m of Request n blk -> False+ _ -> True++peerChildren :: LogChannel -> Handle -> MgrChannel -> PieceMgrChannel+ -> FSPChannel -> StatusChan -> PieceMap -> Int -> IO Children+peerChildren logC handle pMgrC pieceMgrC fsC statusC pm nPieces = do+ queueC <- channel+ senderC <- channel+ receiverC <- channel+ sendBWC <- channel+ return [Worker $ senderP logC handle senderC,+ Worker $ sendQueueP logC queueC senderC sendBWC,+ Worker $ receiverP logC handle receiverC,+ Worker $ peerP pMgrC pieceMgrC fsC pm logC nPieces handle+ queueC receiverC sendBWC statusC]++data RPCF = RPCF { rpLogC :: LogChannel+ , rpMsgC :: Channel (Message, Integer) }++instance Logging RPCF where+ getLogger cf = ("ReceiverP", rpLogC cf)++receiverP :: LogChannel -> Handle -> Channel (Message, Integer) -> SupervisorChan -> IO ThreadId+receiverP logC h ch supC = spawnP (RPCF logC ch) h+ (catchP (foreverP pgm)+ (defaultStopHandler supC))+ where+ pgm = do logDebug "Peer waiting for input"+ readHeader ch+ readHeader ch = do+ h <- get+ feof <- liftIO $ hIsEOF h+ if feof+ then do logDebug "Handle Closed"+ stopP+ else do bs' <- liftIO $ B.hGet h 4+ l <- conv bs'+ readMessage l ch+ readMessage l ch = do+ if (l == 0)+ then return ()+ else do logDebug $ "Reading off " ++ show l ++ " bytes"+ h <- get+ bs <- liftIO $ B.hGet h (fromIntegral l)+ case G.runGet decodeMsg bs of+ Left _ -> do logWarn "Incorrect parse in receiver, dying!"+ stopP+ Right msg -> do sendPC rpMsgC (msg, fromIntegral l) >>= syncP+ conv bs = do+ case G.runGet G.getWord32be bs of+ Left err -> do logWarn $ "Incorrent parse in receiver, dying: " ++ show err+ stopP+ Right i -> return i++data PCF = PCF { inCh :: Channel (Message, Integer)+ , outCh :: Channel SendQueueMessage+ , peerMgrCh :: MgrChannel+ , pieceMgrCh :: PieceMgrChannel+ , logCh :: LogChannel+ , fsCh :: FSPChannel+ , peerCh :: PeerChannel+ , sendBWCh :: BandwidthChannel+ , timerCh :: Channel ()+ , statCh :: StatusChan+ , pieceMap :: PieceMap+ }++instance Logging PCF where+ getLogger cf = ("PeerP", logCh cf)++data PST = PST { weChoke :: Bool -- ^ True if we are choking the peer+ , weInterested :: Bool -- ^ True if we are interested in the peer+ , blockQueue :: S.Set (PieceNum, Block) -- ^ Blocks queued at the peer+ , peerChoke :: Bool -- ^ Is the peer choking us? True if yes+ , peerInterested :: Bool -- ^ True if the peer is interested+ , peerPieces :: [PieceNum] -- ^ List of pieces the peer has access to+ , upRate :: Rate -- ^ Upload rate towards the peer (estimated)+ , downRate :: Rate -- ^ Download rate from the peer (estimated)+ , runningEndgame :: Bool -- ^ True if we are in endgame+ }++peerP :: MgrChannel -> PieceMgrChannel -> FSPChannel -> PieceMap -> LogChannel -> Int -> Handle+ -> Channel SendQueueMessage -> Channel (Message, Integer) -> BandwidthChannel+ -> StatusChan+ -> SupervisorChan -> IO ThreadId+peerP pMgrC pieceMgrC fsC pm logC nPieces h outBound inBound sendBWC statC supC = do+ ch <- channel+ tch <- channel+ ct <- getCurrentTime+ spawnP (PCF inBound outBound pMgrC pieceMgrC logC fsC ch sendBWC tch statC pm)+ (PST True False S.empty True False [] (RC.new ct) (RC.new ct) False)+ (cleanupP startup (defaultStopHandler supC) cleanup)+ where startup = do+ tid <- liftIO $ myThreadId+ logDebug "Syncing a connectBack"+ asks peerCh >>= (\ch -> sendPC peerMgrCh $ Connect tid ch) >>= syncP+ pieces <- getPiecesDone+ syncP =<< (sendPC outCh $ SendQMsg $ BitField (constructBitField nPieces pieces))+ -- Install the StatusP timer+ c <- asks timerCh+ Timer.register 30 () c+ foreverP (recvEvt)+ cleanup = do+ t <- liftIO myThreadId+ syncP =<< sendPC peerMgrCh (Disconnect t)+ getPiecesDone = do+ c <- liftIO $ channel+ syncP =<< (sendPC pieceMgrCh $ GetDone c)+ recvP c (const True) >>= syncP+ recvEvt = do+ syncP =<< chooseP [peerMsgEvent, chokeMgrEvent, upRateEvent, timerEvent]+ chokeMgrEvent = do+ evt <- recvPC peerCh+ wrapP evt (\msg -> do+ logDebug "ChokeMgrEvent"+ case msg of+ PieceCompleted pn -> do+ syncP =<< (sendPC outCh $ SendQMsg $ Have pn)+ ChokePeer -> do syncP =<< sendPC outCh SendOChoke+ logDebug "Choke Peer"+ modify (\s -> s {weChoke = True})+ UnchokePeer -> do syncP =<< (sendPC outCh $ SendQMsg Unchoke)+ logDebug "UnchokePeer"+ modify (\s -> s {weChoke = False})+ PeerStats t retCh -> do+ i <- gets peerInterested+ ur <- gets upRate+ dr <- gets downRate+ let (up, nur) = RC.extractRate t ur+ (down, ndr) = RC.extractRate t dr+ logInfo $ "Peer has rates up/down: " ++ show up ++ "/" ++ show down+ sendP retCh (up, down, i) >>= syncP+ modify (\s -> s { upRate = nur , downRate = ndr })+ CancelBlock pn blk -> do+ modify (\s -> s { blockQueue = S.delete (pn, blk) $ blockQueue s })+ syncP =<< (sendPC outCh $ SendQRequestPrune pn blk))+ timerEvent = do+ evt <- recvPC timerCh+ wrapP evt (\() -> do+ logDebug "TimerEvent"+ tch <- asks timerCh+ Timer.register 30 () tch+ ur <- gets upRate+ dr <- gets downRate+ let (upCnt, nuRate) = RC.extractCount $ ur+ (downCnt, ndRate) = RC.extractCount $ dr+ logDebug $ "Sending peerStats: " ++ show upCnt ++ ", " ++ show downCnt+ (sendPC statCh $ PeerStat { peerUploaded = upCnt+ , peerDownloaded = downCnt }) >>= syncP+ modify (\s -> s { upRate = nuRate, downRate = ndRate }))+ upRateEvent = do+ evt <- recvPC sendBWCh+ wrapP evt (\uploaded -> do+ modify (\s -> s { upRate = RC.update uploaded $ upRate s}))+ peerMsgEvent = do+ evt <- recvPC inCh+ wrapP evt (\(msg, sz) -> do+ modify (\s -> s { downRate = RC.update sz $ downRate s})+ case msg of+ KeepAlive -> return ()+ Choke -> do putbackBlocks+ modify (\s -> s { peerChoke = True })+ Unchoke -> do modify (\s -> s { peerChoke = False })+ fillBlocks+ Interested -> modify (\s -> s { peerInterested = True })+ NotInterested -> modify (\s -> s { peerInterested = False })+ Have pn -> haveMsg pn+ BitField bf -> bitfieldMsg bf+ Request pn blk -> requestMsg pn blk+ Piece n os bs -> do pieceMsg n os bs+ fillBlocks+ Cancel pn blk -> cancelMsg pn blk+ Port _ -> return ()) -- No DHT yet, silently ignore+ putbackBlocks = do+ blks <- gets blockQueue+ syncP =<< sendPC pieceMgrCh (PutbackBlocks (S.toList blks))+ modify (\s -> s { blockQueue = S.empty })+ haveMsg :: PieceNum -> Process PCF PST ()+ haveMsg pn = do+ pm <- asks pieceMap+ if M.member pn pm+ then do modify (\s -> s { peerPieces = pn : peerPieces s})+ considerInterest+ else do logWarn "Unknown Piece"+ stopP+ bitfieldMsg bf = do+ pieces <- gets peerPieces+ case pieces of+ -- TODO: Don't trust the bitfield+ [] -> do modify (\s -> s { peerPieces = createPeerPieces bf})+ considerInterest+ _ -> do logInfo "Got out of band Bitfield request, dying"+ stopP+ requestMsg :: PieceNum -> Block -> Process PCF PST ()+ requestMsg pn blk = do+ choking <- gets weChoke+ unless (choking)+ (do+ bs <- readBlock pn blk -- TODO: Pushdown to send process+ syncP =<< sendPC outCh (SendQMsg $ Piece pn (blockOffset blk) bs))+ readBlock :: PieceNum -> Block -> Process PCF PST B.ByteString+ readBlock pn blk = do+ c <- liftIO $ channel+ syncP =<< sendPC fsCh (ReadBlock pn blk c)+ syncP =<< recvP c (const True)+ pieceMsg :: PieceNum -> Int -> B.ByteString -> Process PCF PST ()+ pieceMsg n os bs = do+ let sz = B.length bs+ blk = Block os sz+ e = (n, blk)+ q <- gets blockQueue+ when (S.member e q)+ (do storeBlock n (Block os sz) bs+ modify (\s -> s { blockQueue = S.delete e (blockQueue s)}))+ -- When e is not a member, the piece may be stray, so ignore it.+ -- Perhaps print something here.+ cancelMsg n blk =+ syncP =<< sendPC outCh (SendQCancel n blk)+ considerInterest = do+ c <- liftIO channel+ pcs <- gets peerPieces+ syncP =<< sendPC pieceMgrCh (AskInterested pcs c)+ interested <- syncP =<< recvP c (const True)+ if interested+ then do modify (\s -> s { weInterested = True })+ syncP =<< sendPC outCh (SendQMsg Interested)+ else modify (\s -> s { weInterested = False})+ fillBlocks = do+ choked <- gets peerChoke+ unless choked checkWatermark+ checkWatermark = do+ q <- gets blockQueue+ eg <- gets runningEndgame+ let sz = S.size q+ mark = if eg then endgameLoMark else loMark+ when (sz < mark)+ (do+ toQueue <- grabBlocks (hiMark - sz)+ logDebug $ "Got " ++ show (length toQueue) ++ " blocks: " ++ show toQueue+ queuePieces toQueue)+ queuePieces toQueue = do+ mapM_ pushPiece toQueue+ modify (\s -> s { blockQueue = S.union (blockQueue s) (S.fromList toQueue) })+ pushPiece (pn, blk) =+ syncP =<< sendPC outCh (SendQMsg $ Request pn blk)+ storeBlock n blk bs =+ syncP =<< sendPC pieceMgrCh (StoreBlock n blk bs)+ grabBlocks n = do+ c <- liftIO $ channel+ ps <- gets peerPieces+ syncP =<< sendPC pieceMgrCh (GrabBlocks n ps c)+ blks <- syncP =<< recvP c (const True)+ case blks of+ Leech blks -> return blks+ Endgame blks ->+ modify (\s -> s { runningEndgame = True }) >> return blks+ loMark = 10+ endgameLoMark = 1+ hiMark = 15 -- These two values are chosen rather arbitrarily at the moment.++createPeerPieces :: L.ByteString -> [PieceNum]+createPeerPieces = map fromIntegral . concat . decodeBytes 0 . L.unpack+ where decodeByte :: Int -> Word8 -> [Maybe Int]+ decodeByte soFar w =+ let dBit n = if testBit w (7-n)+ then Just (n+soFar)+ else Nothing+ in fmap dBit [0..7]+ decodeBytes _ [] = []+ decodeBytes soFar (w : ws) = catMaybes (decodeByte soFar w) : decodeBytes (soFar + 8) ws+++showPort :: PortID -> String+showPort (PortNumber pn) = show pn+showPort _ = "N/A"++disconnectPeer :: MgrChannel -> ThreadId -> IO ()+disconnectPeer c t = sync $ transmit c $ Disconnect t+++-- TODO: Consider if this code is correct with what we did to [connect]+{-+listenHandshake :: Handle -> PeerId -> InfoHash -> FSPChannel -> LogChannel+ -> MgrChannel+ -> IO (Either String ())+listenHandshake h pid ih fsC logC mgrC =+ do r <- initiateHandshake logC h pid ih+ case r of+ Left err -> return $ Left err+ Right (_caps, _rpid) -> do peerP mgrC fsC logC h -- TODO: Coerce with connect+ return $ Right ()+-}
+ src/Process/PeerMgr.hs view
@@ -0,0 +1,95 @@+module Process.PeerMgr (+ -- * Types+ Peer(..)+ -- * Interface+ , start+)+where++import Data.List+import qualified Data.Map as M+import Data.Ord++import Control.Concurrent+import Control.Concurrent.CML+import Control.Monad.State+import Control.Monad.Reader++import PeerTypes+import Process+import Logging+import Process.Peer as Peer+import Process.ChokeMgr hiding (start)+import Process.FS hiding (start)+import Process.PieceMgr hiding (start)+import Process.Status hiding (start)+import Supervisor+import Torrent hiding (infoHash)+++data CF = CF { peerCh :: Channel [Peer]+ , pieceMgrCh :: PieceMgrChannel+ , mgrCh :: Channel MgrMessage+ , fsCh :: FSPChannel+ , peerPool :: SupervisorChan+ , chokeMgrCh :: ChokeMgrChannel+ , logCh :: LogChannel+ }++instance Logging CF where+ getLogger cf = ("PeerMgrP", logCh cf)++data ST = ST { peersInQueue :: [Peer]+ , peers :: M.Map ThreadId (Channel PeerMessage)+ , peerId :: PeerId+ , infoHash :: InfoHash+ }++start :: Channel [Peer] -> PeerId -> InfoHash -> PieceMap -> PieceMgrChannel -> FSPChannel+ -> LogChannel -> ChokeMgrChannel -> StatusChan -> Int -> SupervisorChan+ -> IO ThreadId+start ch pid ih pm pieceMgrC fsC logC chokeMgrC statC nPieces supC =+ do mgrC <- channel+ fakeChan <- channel+ pool <- liftM snd $ oneForOne "PeerPool" [] logC fakeChan+ spawnP (CF ch pieceMgrC mgrC fsC pool chokeMgrC logC)+ (ST [] M.empty pid ih) (catchP (forever lp)+ (defaultStopHandler supC))+ where+ lp = do chooseP [trackerPeers, peerEvent] >>= syncP+ fillPeers+ trackerPeers = do+ ev <- recvPC peerCh+ wrapP ev (\ps ->+ do logDebug "Adding peers to queue"+ modify (\s -> s { peersInQueue = ps ++ peersInQueue s }))+ peerEvent = do+ ev <- recvPC mgrCh+ wrapP ev (\msg -> do+ case msg of+ Connect tid c -> newPeer tid c+ Disconnect tid -> removePeer tid)+ newPeer tid c = do logDebug $ "Adding new peer " ++ show tid+ sendPC chokeMgrCh (AddPeer tid c) >>= syncP+ modify (\s -> s { peers = M.insert tid c (peers s)})+ removePeer tid = do logDebug $ "Removing peer " ++ show tid+ sendPC chokeMgrCh (RemovePeer tid) >>= syncP+ modify (\s -> s { peers = M.delete tid (peers s)})+ numPeers = 40+ fillPeers = do+ sz <- liftM M.size $ gets peers+ when (sz < numPeers)+ (do q <- gets peersInQueue+ let (toAdd, rest) = splitAt (numPeers - sz) q+ logDebug $ "Filling with up to " ++ show (numPeers - sz) ++ " peers"+ mapM_ addPeer toAdd+ modify (\s -> s { peersInQueue = rest }))+ addPeer (Peer hn prt) = do+ pid <- gets peerId+ ih <- gets infoHash+ pool <- asks peerPool+ pmC <- asks pieceMgrCh+ fsC <- asks fsCh+ mgrC <- asks mgrCh+ logC <- asks logCh+ liftIO $ Peer.connect (hn, prt, pid, ih, pm) pool pmC fsC logC statC mgrC nPieces
+ src/Process/PieceMgr.hs view
@@ -0,0 +1,432 @@+module Process.PieceMgr+ ( PieceMgrMsg(..)+ , PieceMgrChannel+ , ChokeInfoChannel+ , ChokeInfoMsg(..)+ , Blocks(..)+ , start+ , createPieceDb+ )+where+++import Control.Concurrent+import Control.Concurrent.CML+import Control.Monad.State+import Data.List+import qualified Data.ByteString as B+import qualified Data.Map as M+import qualified Data.Set as S++import Prelude hiding (log)++import System.Random+import System.Random.Shuffle++import Logging+import Process.FS hiding (start)+import Process.Status as STP hiding (start) +import Supervisor+import Torrent+import Process++----------------------------------------------------------------------++-- | The Piece Database tracks the current state of the Torrent with respect to pieces.+-- In the database, we book-keep what pieces are missing, what are done and what are+-- currently in the process of being downloaded. The crucial moment is when we think+-- we have a full piece: we check it against its SHA1 and if it is good, we can mark+-- that piece as done.+--+-- Better implementations for selecting among the pending Pieces is probably crucial+-- to an effective client, but we keep it simple for now.+data PieceDB = PieceDB+ { pendingPieces :: [PieceNum] -- ^ Pieces currently pending download+ , donePiece :: [PieceNum] -- ^ Pieces that are done+ , donePush :: [ChokeInfoMsg] -- ^ Pieces that should be pushed to the Choke Mgr.+ , inProgress :: M.Map PieceNum InProgressPiece -- ^ Pieces in progress+ , downloading :: [(PieceNum, Block)] -- ^ Blocks we are currently downloading+ , infoMap :: PieceMap -- ^ Information about pieces+ , endGaming :: Bool -- ^ If we have done any endgame work this is true+ } deriving Show++-- | The InProgressPiece data type describes pieces in progress of being downloaded.+-- we keep track of blocks which are pending as well as blocks which are done. We+-- also keep track of a count of the blocks. When a block is done, we cons it unto+-- @ipHaveBlocks@. When @ipHave == ipDone@, we check the piece for correctness. The+-- field @ipHaveBlocks@ could in principle be omitted, but for now it is kept since+-- we can use it for asserting implementation correctness. We note that both the+-- check operations are then O(1) and probably fairly fast.+data InProgressPiece = InProgressPiece+ { ipDone :: Int -- ^ Number of blocks when piece is done+ , ipHaveBlocks :: S.Set Block -- ^ The blocks we have+ , ipPendingBlocks :: [Block] -- ^ Blocks still pending+ } deriving Show++-- INTERFACE+----------------------------------------------------------------------++-- | When the PieceMgrP returns blocks to a peer, it will return them in either+-- "Leech Mode" or in "Endgame mode". The "Leech mode" is when the client is+-- leeching like normal. The "Endgame mode" is when the client is entering the+-- endgame. This means that the Peer should act differently to the blocks.+data Blocks = Leech [(PieceNum, Block)]+ | Endgame [(PieceNum, Block)]++-- | Messages for RPC towards the PieceMgr.+data PieceMgrMsg = GrabBlocks Int [PieceNum] (Channel Blocks)+ -- ^ Ask for grabbing some blocks+ | StoreBlock PieceNum Block B.ByteString+ -- ^ Ask for storing a block on the file system+ | PutbackBlocks [(PieceNum, Block)]+ -- ^ Put these blocks back for retrieval+ | AskInterested [PieceNum] (Channel Bool)+ -- ^ Ask if any of these pieces are interesting+ | GetDone (Channel [PieceNum])+ -- ^ Get the pieces which are already done++data ChokeInfoMsg = PieceDone PieceNum+ | BlockComplete PieceNum Block+ | TorrentComplete+ deriving (Eq, Show)++type PieceMgrChannel = Channel PieceMgrMsg+type ChokeInfoChannel = Channel ChokeInfoMsg++data PieceMgrCfg = PieceMgrCfg+ { logCh :: LogChannel+ , pieceMgrCh :: PieceMgrChannel+ , fspCh :: FSPChannel+ , chokeCh :: ChokeInfoChannel+ , statusCh :: StatusChan+ }++instance Logging PieceMgrCfg where+ getLogger cf = ("PieceMgrP", logCh cf)++type PieceMgrProcess v = Process PieceMgrCfg PieceDB v++start :: LogChannel -> PieceMgrChannel -> FSPChannel -> ChokeInfoChannel -> StatusChan -> PieceDB+ -> SupervisorChan -> IO ThreadId+start logC mgrC fspC chokeC statC db supC =+ spawnP (PieceMgrCfg logC mgrC fspC chokeC statC) db+ (catchP (forever pgm)+ (defaultStopHandler supC))+ where pgm = do+ assertPieceDB+ dl <- gets donePush+ (if dl == []+ then receiveEvt+ else chooseP [receiveEvt, sendEvt (head dl)]) >>= syncP+ sendEvt elem = do+ ev <- sendPC chokeCh elem+ wrapP ev remDone+ remDone :: () -> Process PieceMgrCfg PieceDB ()+ remDone () = modify (\db -> db { donePush = tail (donePush db) })+ receiveEvt = do+ ev <- recvPC pieceMgrCh+ wrapP ev (\msg ->+ case msg of+ GrabBlocks n eligible c ->+ do logDebug "Grabbing Blocks"+ blocks <- grabBlocks' n eligible+ logDebug "Grabbed..."+ syncP =<< sendP c blocks+ StoreBlock pn blk d ->+ do logDebug $ "Storing block: " ++ show (pn, blk)+ storeBlock pn blk d+ modify (\s -> s { downloading = downloading s \\ [(pn, blk)] })+ endgameBroadcast pn blk+ done <- updateProgress pn blk+ when done+ (do assertPieceComplete pn+ pend <- gets pendingPieces+ iprog <- gets inProgress+ logInfo $ "Piece #" ++ show pn+ ++ " completed, there are " + ++ (show $ length pend) ++ " pending "+ ++ (show $ M.size iprog) ++ " in progress"+ l <- gets infoMap >>=+ (\pm -> case M.lookup pn pm of+ Nothing -> fail "Storeblock: M.lookup"+ Just x -> return $ len x)+ sendPC statusCh (CompletedPiece l) >>= syncP+ pieceOk <- checkPiece pn+ case pieceOk of+ Nothing ->+ do fail "PieceMgrP: Piece Nonexisting!"+ Just True -> do completePiece pn+ markDone pn+ checkFullCompletion+ Just False -> putbackPiece pn)+ PutbackBlocks blks ->+ mapM_ putbackBlock blks+ GetDone c -> do done <- gets donePiece+ syncP =<< sendP c done+ AskInterested pieces retC -> do+ inProg <- liftM (S.fromList . M.keys) $ gets inProgress+ pend <- liftM S.fromList $ gets pendingPieces+ -- @i@ is the intersection with with we need and the peer has.+ let i = S.null $ S.intersection (S.fromList pieces)+ $ S.union inProg pend+ syncP =<< sendP retC (not i))+ storeBlock n blk contents = syncP =<< (sendPC fspCh $ WriteBlock n blk contents)+ endgameBroadcast pn blk =+ gets endGaming >>=+ flip when (modify (\db -> db { donePush = (BlockComplete pn blk) : donePush db }))+ markDone pn = do+ modify (\db -> db { donePush = (PieceDone pn) : donePush db })+ checkPiece n = do+ ch <- liftIO channel+ syncP =<< (sendPC fspCh $ CheckPiece n ch)+ syncP =<< recvP ch (const True)++-- HELPERS+----------------------------------------------------------------------++createPieceDb :: PiecesDoneMap -> PieceMap -> PieceDB+createPieceDb mmap pmap = PieceDB pending done [] M.empty [] pmap False+ where pending = M.keys $ M.filter (==False) mmap+ done = M.keys $ M.filter (==True) mmap++----------------------------------------------------------------------++-- | The call @completePiece db pn@ will mark that the piece @pn@ is completed+completePiece :: PieceNum -> PieceMgrProcess ()+completePiece pn = modify (\db -> db { inProgress = M.delete pn (inProgress db),+ donePiece = pn : donePiece db })++-- | Handle torrent completion+checkFullCompletion :: PieceMgrProcess ()+checkFullCompletion = do+ doneP <- gets donePiece+ im <- gets infoMap+ when (M.size im == length doneP)+ (do logInfo "Torrent Completed"+ sendPC statusCh STP.TorrentCompleted >>= syncP+ sendPC chokeCh TorrentComplete >>= syncP)++-- | The call @putBackPiece db pn@ will mark the piece @pn@ as not being complete+-- and put it back into the download queue again.+putbackPiece :: PieceNum -> PieceMgrProcess ()+putbackPiece pn = modify (\db -> db { inProgress = M.delete pn (inProgress db),+ pendingPieces = pn : pendingPieces db })++-- | Put back a block for downloading.+-- TODO: This is rather slow, due to the (\\) call, but hopefully happens rarely.+putbackBlock :: (PieceNum, Block) -> PieceMgrProcess ()+putbackBlock (pn, blk) = do+ done <- gets donePiece+ unless (pn `elem` done) -- Happens at endgame, stray block+ $ modify (\db -> db { inProgress = ndb (inProgress db)+ , downloading = downloading db \\ [(pn, blk)]})+ where ndb db = M.alter f pn db+ -- The first of these might happen in the endgame+ f Nothing = fail "The 'impossible' happened"+ f (Just ipp) = Just ipp { ipPendingBlocks = blk : ipPendingBlocks ipp }++-- | Assert that a Piece is Complete. Can be omitted when we know it works+-- and we want a faster client.+assertPieceComplete :: PieceNum -> PieceMgrProcess ()+assertPieceComplete pn = do+ inprog <- gets inProgress+ ipp <- case M.lookup pn inprog of+ Nothing -> fail "assertPieceComplete: Could not lookup piece number"+ Just x -> return x+ dl <- gets downloading+ pm <- gets infoMap+ sz <- case M.lookup pn pm of+ Nothing -> fail "assertPieceComplete: Could not lookup piece in piecemap"+ Just x -> return $ len x+ unless (assertAllDownloaded dl pn)+ (fail "Could not assert that all pieces were downloaded when completing a piece")+ unless (assertComplete ipp sz)+ (fail $ "Could not assert completion of the piece #" ++ show pn+ ++ " with block state " ++ show ipp)+ where assertComplete ip sz = checkContents 0 (fromIntegral sz) (S.toAscList (ipHaveBlocks ip))+ -- Check a single block under assumptions of a cursor at offs+ checkBlock (offs, left, state) blk = (offs + blockSize blk,+ left - blockSize blk,+ state && offs == blockOffset blk)+ checkContents os l blks = case foldl checkBlock (os, l, True) blks of+ (_, 0, True) -> True+ _ -> False+ assertAllDownloaded blocks pn = all (\(pn', _) -> pn /= pn') blocks++-- | Update the progress on a Piece. When we get a block from the piece, we will+-- track this in the Piece Database. This function returns a pair @(complete, nDb)@+-- where @complete@ is @True@ if the piece is percieved to be complete and @False@+-- otherwise.+updateProgress :: PieceNum -> Block -> PieceMgrProcess Bool+updateProgress pn blk = do+ ipdb <- gets inProgress+ case M.lookup pn ipdb of+ Nothing -> do logDebug "updateProgress can't find progress block, error?"+ return False+ Just pg ->+ let blkSet = ipHaveBlocks pg+ in if blk `S.member` blkSet+ then return False -- Stray block download.+ -- Will happen without FAST extension+ -- at times+ else checkComplete pg { ipHaveBlocks = S.insert blk blkSet }+ where checkComplete pg = do+ modify (\db -> db { inProgress = M.adjust (const pg) pn (inProgress db) })+ logDebug $ "Iphave : " ++ show (ipHave pg) ++ " ipDone: " ++ show (ipDone pg)+ return (ipHave pg == ipDone pg)+ ipHave = S.size . ipHaveBlocks++blockPiece :: BlockSize -> PieceSize -> [Block]+blockPiece blockSz pieceSize = build pieceSize 0 []+ where build 0 os accum = reverse accum+ build leftBytes os accum | leftBytes >= blockSz =+ build (leftBytes - blockSz)+ (os + blockSz)+ $ Block os blockSz : accum+ | otherwise = build 0 (os + leftBytes) $ Block os leftBytes : accum++-- | The call @grabBlocks' n eligible db@ tries to pick off up to @n@ pieces from+-- the @n@. In doing so, it will only consider pieces in @eligible@. It returns a+-- pair @(blocks, db')@, where @blocks@ are the blocks it picked and @db'@ is the resulting+-- db with these blocks removed.+grabBlocks' :: Int -> [PieceNum] -> PieceMgrProcess Blocks+grabBlocks' k eligible = do+ blocks <- tryGrabProgress k eligible []+ pend <- gets pendingPieces+ if blocks == [] && pend == []+ then do blks <- grabEndGame k (S.fromList eligible)+ modify (\db -> db { endGaming = True })+ logDebug $ "PieceMgr entered endgame."+ return $ Endgame blks+ else do modify (\s -> s { downloading = blocks ++ (downloading s) })+ return $ Leech blocks+ where+ -- Grabbing blocks is a state machine implemented by tail calls+ -- Try grabbing pieces from the pieces in progress first+ tryGrabProgress 0 _ captured = return captured+ tryGrabProgress n ps captured = do+ inprog <- gets inProgress+ case ps `intersect` fmap fst (M.toList inprog) of+ [] -> tryGrabPending n ps captured+ (h:_) -> grabFromProgress n ps h captured+ -- The Piece @p@ was found, grab it+ grabFromProgress n ps p captured = do+ inprog <- gets inProgress+ ipp <- case M.lookup p inprog of+ Nothing -> fail "grabFromProgress: could not lookup piece"+ Just x -> return x+ let (grabbed, rest) = splitAt n (ipPendingBlocks ipp)+ nIpp = ipp { ipPendingBlocks = rest }+ -- This rather ugly piece of code should be substituted with something better+ if grabbed == []+ -- All pieces are taken, try the next one.+ then tryGrabProgress n (ps \\ [p]) captured+ else do modify (\db -> db { inProgress = M.insert p nIpp inprog })+ tryGrabProgress (n - length grabbed) ps ([(p,g) | g <- grabbed] ++ captured)+ -- Try grabbing pieces from the pending blocks+ tryGrabPending n ps captured = do+ pending <- gets pendingPieces+ case ps `intersect` pending of+ [] -> return $ captured -- No (more) pieces to download, return+ ls -> do+ h <- pickRandom ls+ infMap <- gets infoMap+ inProg <- gets inProgress+ blockList <- createBlock h+ let sz = length blockList+ ipp = InProgressPiece sz S.empty blockList+ modify (\db -> db { pendingPieces = pendingPieces db \\ [h],+ inProgress = M.insert h ipp inProg })+ tryGrabProgress n ps captured+ grabEndGame n ps = do -- In endgame we are allowed to grab from the downloaders+ dls <- liftM (filter (\(p, _) -> S.member p ps)) $ gets downloading+ g <- liftIO newStdGen+ let shuffled = shuffle' dls (length dls) g+ return $ take n shuffled+ pickRandom pieces = do+ n <- liftIO $ getStdRandom (\gen -> randomR (0, length pieces - 1) gen)+ return $ pieces !! n+ createBlock :: Int -> PieceMgrProcess [Block]+ createBlock pn = do+ gets infoMap >>= (\im -> case M.lookup pn im of+ Nothing -> fail "createBlock: could not lookup piece"+ Just ipp -> return $ cBlock ipp)+ where cBlock = blockPiece defaultBlockSize . fromInteger . len++assertPieceDB :: PieceMgrProcess ()+assertPieceDB = assertPending >> assertDone >> assertInProgress >> assertDownloading+ where+ -- If a piece is pending in the database, we have the following rules:+ --+ -- - It is not finished.+ -- - It is not being downloaded+ -- - It is not in progresss.+ assertPending = do+ pending <- gets pendingPieces+ mapM_ checkPending pending+ checkPending pn = do+ done <- gets donePiece+ when (pn `elem` done)+ (fail $ "Pending piece " ++ show pn ++ " is in the done list")+ down <- gets downloading+ when (pn `elem` map fst down)+ (fail $ "Pending piece " ++ show pn ++ " is in the downloading list")+ inProg <- gets inProgress+ when (case M.lookup pn inProg of+ Nothing -> False+ Just _ -> True)+ (fail $ "Pending piece " ++ show pn ++ " is in the progress map")+ -- If a piece is done, we have the following rules:+ --+ -- - It is not pending.+ -- - It is not in progress.+ -- - There are no more downloading blocks.+ assertDone = do+ done <- gets donePiece+ mapM_ checkDone done+ checkDone pn = do+ pending <- gets pendingPieces+ when (pn `elem` pending)+ (fail $ "Done piece " ++ show pn ++ " is in the pending list")+ down <- gets downloading+ when (pn `elem` map fst down)+ (fail $ "Done piece " ++ show pn ++ " is in the downloading list")+ inProg <- gets inProgress+ when (case M.lookup pn inProg of + Nothing -> False+ Just _ -> True)+ (fail $ "Done piece " ++ show pn ++ " is in the progress map")+ -- If a piece is in Progress, we have:+ --+ -- - The piece is not Done+ -- - The piece is not pending+ -- - There is a relationship with what pieces are downloading+ -- - If a block is ipPending, it is not in the downloading list+ -- - If a block is ipHave, it is not in the downloading list+ assertInProgress = do+ inProg <- gets inProgress+ mapM_ checkInProgress $ M.toList inProg+ checkInProgress (pn, ipp) = do+ when ( (S.size $ ipHaveBlocks ipp) >= ipDone ipp)+ (fail $ "Piece in progress " ++ show pn+ ++ " has downloaded more blocks than the piece has")+ done <- gets donePiece+ when (pn `elem` done)+ (fail $ "Piece in progress " ++ show pn ++ " is in the done list")+ pending <- gets pendingPieces+ when (pn `elem` pending)+ (fail $ "Piece in progress " ++ show pn ++ " is in the pending list")+ assertDownloading = do+ down <- gets downloading+ mapM_ checkDownloading down+ checkDownloading (pn, blk) = do+ prog <- gets inProgress+ case M.lookup pn prog of+ Nothing -> fail $ "Piece " ++ show pn ++ " not in progress while We think it was"+ Just ipp -> do+ when (blk `elem` ipPendingBlocks ipp)+ (fail $ "P/Blk " ++ show (pn, blk) ++ " is in the Pending Block list")+ when (S.member blk $ ipHaveBlocks ipp)+ (fail $ "P/Blk " ++ show (pn, blk) ++ " is in the HaveBlocks set")++
+ src/Process/Status.hs view
@@ -0,0 +1,116 @@+-- Haskell Torrent+-- Copyright (c) 2009, Jesper Louis Andersen,+-- All rights reserved.+--+-- Redistribution and use in source and binary forms, with or without+-- modification, are permitted provided that the following conditions are+-- met:+--+-- * Redistributions of source code must retain the above copyright+-- notice, this list of conditions and the following disclaimer.+-- * Redistributions in binary form must reproduce the above copyright+-- notice, this list of conditions and the following disclaimer in the+-- documentation and/or other materials provided with the distribution.+--+-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS+-- IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,+-- THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR+-- PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR+-- CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,+-- EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,+-- PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR+-- PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF+-- LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING+-- NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS+-- SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.++-- | The status code runs a Status Process. This process keeps track+-- of a number of interval valies for a given torrent file and it+-- periodically updates the tracker process with the relevant+-- information about data uploaded, downloaded and how much is+-- left. The tracker is then responsible for using this data+-- correctly to tell the tracker what to do+module Process.Status (+ -- * Types+ StatusMsg(..)+ , TrackerMsg(..)+ -- * Channels+ , StatusChan+ -- * State+ , ST(uploaded, downloaded, state, left)+ -- * Interface+ , start+ )+where++import Control.Concurrent+import Control.Concurrent.CML++import Control.Monad.State+import Control.Monad.Reader++import Prelude hiding (log)+import Logging+import Process+import Supervisor+import Torrent++data StatusMsg = TrackerStat { trackIncomplete :: Maybe Integer+ , trackComplete :: Maybe Integer }+ | CompletedPiece Integer+ | PeerStat { peerUploaded :: Integer+ , peerDownloaded :: Integer }+ | TorrentCompleted++type StatusChan = Channel StatusMsg++-- | TrackerChannel is the channel of the tracker+data TrackerMsg = Stop | TrackerTick Integer | Start | Complete++data CF = CF { logCh :: LogChannel+ , statusCh :: Channel StatusMsg+ , trackerCh1 :: Channel TrackerMsg+ , trackerCh :: Channel ST }++instance Logging CF where+ getLogger cf = ("StatusP", logCh cf)++data ST = ST { uploaded :: Integer,+ downloaded :: Integer,+ left :: Integer,+ incomplete :: Maybe Integer,+ complete :: Maybe Integer,+ state :: TorrentState }++-- | Start a new Status process with an initial torrent state and a+-- channel on which to transmit status updates to the tracker.+start :: LogChannel -> Integer -> TorrentState -> Channel ST+ -> Channel StatusMsg -> Channel TrackerMsg -> SupervisorChan -> IO ThreadId+start logC l tState trackerC statusC trackerC1 supC = do+ spawnP (CF logC statusC trackerC1 trackerC) (ST 0 0 l Nothing Nothing tState)+ (catchP (foreverP pgm) (defaultStopHandler supC))+ where+ pgm = do ev <- chooseP [sendEvent, recvEvent]+ syncP ev+ sendEvent = get >>= sendPC trackerCh+ recvEvent = do evt <- recvPC statusCh+ wrapP evt (\m ->+ case m of+ TrackerStat ic c ->+ modify (\s -> s { incomplete = ic, complete = c })+ CompletedPiece bytes -> do+ logDebug "StatusProcess updated left"+ modify (\s -> s { left = (left s) - bytes })+ PeerStat up down -> do+ modify (\s -> s { uploaded = (uploaded s) + up,+ downloaded = (downloaded s) + down })+ u <- gets uploaded+ d <- gets downloaded+ logDebug $ "StatusProcess up/down count: " ++ show u ++ ", " ++ show d+ TorrentCompleted -> do+ logDebug "TorrentCompletion at StatusP"+ l <- gets left+ when (l /= 0) (fail "Warning: Left is not 0 upon Torrent Completion")+ syncP =<< sendPC trackerCh1 Complete+ modify (\s -> s { state = Seeding }))+
+ src/Process/Timer.hs view
@@ -0,0 +1,25 @@+-- | The timer module is responsible for timing in the project. With+-- the timer system, we can register timers in the future and then+-- we can get a tick triggered at the point in time where we need to+-- act. This allows us to postpone events into the future at a+-- designated time.+--+module Process.Timer (register)++where++import Control.Concurrent+import Control.Concurrent.CML+import Control.Monad.Trans++-- | Registers a timer tick on a channel in a number of seconds with+-- an annotated version.+registerL :: Integer -> a -> Channel a -> IO ()+registerL secs v tickChan = do spawn timerProcess+ return ()+ where timerProcess = do threadDelay $ fromInteger $ secs * 1000000+ sync $ transmit tickChan v+++register :: MonadIO m => Integer -> a -> Channel a -> m ()+register secs v c = liftIO $ registerL secs v c
+ src/Process/Tracker.hs view
@@ -0,0 +1,269 @@+-- | The TrackerP module is responsible for keeping in touch with the Tracker of a torrent.+-- The tracker is contacted periodically, and we exchange information with it. Specifically,+-- we tell the tracker how much we have downloaded, uploaded and what is left. We also+-- tell it about our current state (i.e., are we a seeder or a leecher?).+--+-- The tracker responds to us with a new set of Peers and general information about the+-- torrent in question. It may also respond with an error in which case we should present+-- it to the user.+--+module Process.Tracker+where++import Control.Applicative+import Control.Concurrent+import Control.Concurrent.CML+import Control.Monad.Reader+import Control.Monad.State++import Data.Char (ord)+import Data.List (intersperse)+import Data.Time.Clock.POSIX+import qualified Data.ByteString as B++import Network+import Network.HTTP hiding (port)+import Network.URI hiding (unreserved)++import Numeric (showHex)+++import Protocol.BCode as BCode hiding (encode)+import Logging+import Process+import Supervisor+import Torrent+import qualified Process.Status as Status+import qualified Process.PeerMgr as PeerMgr+import qualified Process.Timer as Timer++++-- | The tracker state is used to tell the tracker our current state. In order+-- to output it correctly, we override the default show instance with the+-- version below. This may be wrong to do in the long run, but for now it works+-- fine.+--+-- The state is either started or stopped upon the client starting. The+-- tracker will create an entry for us if we tell it that we started, and it+-- will tear down this entry if we tell it that we stopped. It will know if+-- we are a seeder or a leecher based on how much data is left for us to+-- download.+--+-- the 'Completed' entry is used once in the lifetime of a torrent. It+-- explains to the tracker that we completed the torrent in question.+data TrackerEvent = Started | Stopped | Completed | Running+ deriving Eq++++-- | The tracker will in general respond with a BCoded dictionary. In our world, this is+-- not the data structure we would like to work with. Hence, we parse the structure into+-- the ADT below.+data TrackerResponse = ResponseOk { newPeers :: [PeerMgr.Peer],+ completeR :: Maybe Integer,+ incompleteR :: Maybe Integer,+ timeoutInterval :: Integer,+ timeoutMinInterval :: Maybe Integer }+ | ResponseDecodeError B.ByteString+ | ResponseWarning B.ByteString+ | ResponseError B.ByteString++-- | If we fail to contact the tracker, we will wait for 15 minutes. The number is quite arbitrarily chosen+failTimerInterval :: Integer+failTimerInterval = 15 * 60++-- | Configuration of the tracker process+data CF = CF {+ logCh :: LogChannel+ , statusCh :: Channel Status.ST+ , statusPCh :: Channel Status.StatusMsg+ , trackerMsgCh :: Channel Status.TrackerMsg+ , peerMgrCh :: Channel [PeerMgr.Peer]+ }++instance Logging CF where+ getLogger cf = ("TrackerP", logCh cf)++-- | Internal state of the tracker CHP process+data ST = ST {+ torrentInfo :: TorrentInfo+ , peerId :: PeerId+ , state :: TrackerEvent+ , localPort :: PortID+ , nextContactTime :: POSIXTime+ , nextTick :: Integer+ }++start :: TorrentInfo -> PeerId -> PortID -> LogChannel -> Channel Status.ST+ -> Channel Status.StatusMsg -> Channel Status.TrackerMsg -> Channel [PeerMgr.Peer]+ -> SupervisorChan -> IO ThreadId+start ti pid port logC sc statusC msgC pc supC =+ do tm <- getPOSIXTime+ spawnP (CF logC sc statusC msgC pc) (ST ti pid Stopped port tm 0)+ (catchP (forever loop)+ (defaultStopHandler supC)) -- TODO: Gracefully close down here!++loop :: Process CF ST ()+loop = do msg <- recvPC trackerMsgCh >>= syncP+ logDebug $ "Got tracker event"+ case msg of+ Status.TrackerTick x ->+ do t <- gets nextTick+ when (x+1 == t) talkTracker+ Status.Stop ->+ modify (\s -> s { state = Stopped }) >> talkTracker+ Status.Start ->+ modify (\s -> s { state = Started }) >> talkTracker+ Status.Complete ->+ modify (\s -> s { state = Completed }) >> talkTracker+ where+ talkTracker = pokeTracker >>= timerUpdate++eventTransition :: Process CF ST ()+eventTransition = do+ st <- gets state+ modify (\s -> s { state = newS st})+ where newS st =+ case st of+ Running -> Running+ Stopped -> Stopped+ Completed -> Running+ Started -> Running++-- | Poke the tracker. It returns the new timer intervals to use+pokeTracker :: Process CF ST (Integer, Maybe Integer)+pokeTracker = do+ upDownLeft <- syncP =<< recvPC statusCh+ url <- buildRequestURL upDownLeft+ logDebug $ "Request URL: " ++ url+ uri <- case parseURI url of+ Nothing -> fail $ "Could not parse the url " ++ url+ Just u -> return u+ resp <- trackerRequest uri+ case resp of+ Left err -> do logInfo $ "Tracker HTTP Error: " ++ err+ return (failTimerInterval, Just failTimerInterval)+ Right (ResponseWarning wrn) ->+ do logInfo $ "Tracker Warning Response: " ++ fromBS wrn+ return (failTimerInterval, Just failTimerInterval)+ Right (ResponseError err) ->+ do logInfo $ "Tracker Error Response: " ++ fromBS err+ return (failTimerInterval, Just failTimerInterval)+ Right (ResponseDecodeError err) ->+ do logInfo $ "Response Decode error: " ++ fromBS err+ return (failTimerInterval, Just failTimerInterval)+ Right bc -> do sendPC peerMgrCh (newPeers bc) >>= syncP+ let trackerStats = Status.TrackerStat { Status.trackComplete = completeR bc,+ Status.trackIncomplete = incompleteR bc }+ sendPC statusPCh trackerStats >>= syncP+ eventTransition+ return (timeoutInterval bc, timeoutMinInterval bc)++timerUpdate :: (Integer, Maybe Integer) -> Process CF ST ()+timerUpdate (timeout, minTimeout) = do+ st <- gets state+ when (st == Running)+ (do t <- tick+ ch <- asks trackerMsgCh+ Timer.register timeout (Status.TrackerTick t) ch+ logDebug $ "Set timer to: " ++ show timeout)+ where tick = do t <- gets nextTick+ modify (\s -> s { nextTick = t + 1 })+ return t++-- Process a result dict into a tracker response object.+processResultDict :: BCode -> TrackerResponse+processResultDict d =+ case BCode.trackerError d of+ Just err -> ResponseError err+ Nothing -> case BCode.trackerWarning d of+ Just warn -> ResponseWarning warn+ Nothing -> case decodeOk of+ Nothing -> ResponseDecodeError . toBS $ "Could not decode response properly"+ Just rok -> rok+ where decodeOk =+ ResponseOk <$> (decodeIps <$> BCode.trackerPeers d)+ <*> (pure $ BCode.trackerComplete d)+ <*> (pure $ BCode.trackerIncomplete d)+ <*> (BCode.trackerInterval d)+ <*> (pure $ BCode.trackerMinInterval d)+++decodeIps :: B.ByteString -> [PeerMgr.Peer]+decodeIps = decodeIps' . fromBS++-- Decode a list of IP addresses. We expect these to be a compact response by default.+decodeIps' :: String -> [PeerMgr.Peer]+decodeIps' [] = []+decodeIps' (b1 : b2 : b3 : b4 : p1 : p2 : rest) = PeerMgr.Peer ip port : decodeIps' rest+ where ip = concat . intersperse "." . map (show . ord) $ [b1, b2, b3, b4]+ port = PortNumber . fromIntegral $ ord p1 * 256 + ord p2+decodeIps' xs = error $ "decodeIps': invalid IPs: " ++ xs -- Quench all other cases++trackerRequest :: URI -> Process CF ST (Either String TrackerResponse)+trackerRequest uri =+ do resp <- liftIO $ simpleHTTP request+ case resp of+ Left x -> return $ Left ("Error connecting: " ++ show x)+ Right r ->+ case rspCode r of+ (2,_,_) ->+ case BCode.decode . toBS . rspBody $ r of+ Left pe -> return $ Left (show pe)+ Right bc -> do logDebug $ "Response: " ++ BCode.prettyPrint bc+ return $ Right $ processResultDict bc+ (3,_,_) ->+ case findHeader HdrLocation r of+ Nothing -> return $ Left (show r)+ Just newURL -> case parseURI newURL of+ Nothing -> return $ Left (show newURL)+ Just uri -> trackerRequest uri+ _ -> return $ Left (show r)+ where request = Request {rqURI = uri,+ rqMethod = GET,+ rqHeaders = [],+ rqBody = ""}++-- Construct a new request URL. Perhaps this ought to be done with the HTTP client library+buildRequestURL :: Status.ST -> Process CF ST String+buildRequestURL ss = do ti <- gets torrentInfo+ hdrs <- headers+ let hl = concat $ hlist hdrs+ return $ concat [fromBS $ announceURL ti, "?", hl]+ where hlist x = intersperse "&" $ map (\(k,v) -> k ++ "=" ++ v) x+ headers = do+ s <- get+ p <- prt+ return $ [("info_hash", rfc1738Encode $ infoHash $ torrentInfo s),+ ("peer_id", rfc1738Encode $ peerId s),+ ("uploaded", show $ Status.uploaded ss),+ ("downloaded", show $ Status.downloaded ss),+ ("left", show $ Status.left ss),+ ("port", show p),+ ("compact", "1")] +++ (trackerfyEvent $ state s)+ prt :: Process CF ST Integer+ prt = do lp <- gets localPort+ case lp of+ PortNumber pnum -> return $ fromIntegral pnum+ _ -> do fail "Unknown port type"+ trackerfyEvent ev =+ case ev of+ Running -> []+ Completed -> [("event", "completed")]+ Started -> [("event", "started")]+ Stopped -> [("event", "stopped")]++-- Carry out URL-encoding of a string. Note that the clients seems to do it the wrong way+-- so we explicitly code it up here in the same wrong way, jlouis.+rfc1738Encode :: String -> String+rfc1738Encode = concatMap (\c -> if unreserved c then [c] else encode c)+ where unreserved = (`elem` chars)+ -- I killed ~ from this list as the Mainline client doesn't announce it - jlouis+ chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_./"+ encode :: Char -> String+ encode c = '%' : pHex c+ pHex c =+ let p = (showHex . ord $ c) ""+ in if length p == 1 then '0' : p else p
+ src/Protocol/BCode.hs view
@@ -0,0 +1,365 @@+-- Haskell Torrent+-- Copyright (c) 2009, Jesper Louis Andersen,+-- All rights reserved.+--+-- Redistribution and use in source and binary forms, with or without+-- modification, are permitted provided that the following conditions are+-- met:+--+-- * Redistributions of source code must retain the above copyright+-- notice, this list of conditions and the following disclaimer.+-- * Redistributions in binary form must reproduce the above copyright+-- notice, this list of conditions and the following disclaimer in the+-- documentation and/or other materials provided with the distribution.+--+-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS+-- IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,+-- THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR+-- PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR+-- CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,+-- EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,+-- PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR+-- PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF+-- LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING+-- NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS+-- SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.+++-- | Add a module description here+-- also add descriptions to each function.+module Protocol.BCode +(+ BCode,+ Path(..),+ encode,+ -- encodeBS,+ decode,+ search,+ announce,+ comment,+ creationDate,+ info,+ hashInfoDict,+ infoLength,+ infoName,+ infoPieceLength,+ infoPieces,+ numberPieces,+ infoFiles,+ prettyPrint,++ trackerComplete,+ trackerIncomplete,+ trackerInterval,+ trackerMinInterval,+ trackerPeers,+ trackerWarning,+ trackerError,+ toBS,+ fromBS )+where++import Control.Monad+import Control.Applicative hiding (many)+import qualified Data.ByteString.Lazy as L+import qualified Data.ByteString as B+import Data.Char+import Data.Int+import Data.List+import Data.Maybe+import qualified Data.Map as M+import Text.PrettyPrint.HughesPJ hiding (char)++import Data.Serialize+import Data.Serialize.Put+import Data.Serialize.Get++import Digest++-- | BCode represents the structure of a bencoded file+data BCode = BInt Integer -- ^ An integer+ | BString B.ByteString -- ^ A string of bytes+ | BArray [BCode] -- ^ An array+ | BDict (M.Map B.ByteString BCode) -- ^ A key, value map+ deriving (Show, Eq)++data Path = PString B.ByteString+ | PInt Int++toW8 :: Char -> Word8+toW8 = fromIntegral . ord++fromW8 :: Word8 -> Char+fromW8 = chr . fromIntegral++toBS :: String -> B.ByteString+toBS = B.pack . map toW8++fromBS :: B.ByteString -> String+fromBS = map fromW8 . B.unpack+++instance Serialize BCode where+ put (BInt i) = wrap 'i' 'e' $ putShow i+ put (BArray arr) = wrap 'l' 'e' . mapM_ put $ arr+ put (BDict mp) = wrap 'd' 'e' dict+ where dict = mapM_ encPair . M.toList $ mp+ encPair (k, v) = put (BString k) >> put v+ put (BString s) = do+ putShow (B.length s)+ putWord8 (toW8 ':')+ putByteString s+ + get = getBInt <|> getBArray <|> getBDict <|> getBString++-- | Get something wrapped in two Chars+getWrapped :: Char -> Char -> Get a -> Get a+getWrapped a b p = char a *> p <* char b++-- | Parses a BInt+getBInt :: Get BCode+getBInt = BInt . read <$> getWrapped 'i' 'e' intP+ where intP = ((:) <$> char '-' <*> getDigits) <|> getDigits++-- | Parses a BArray+getBArray :: Get BCode+getBArray = BArray <$> getWrapped 'l' 'e' (many get)+++-- | Parses a BDict+getBDict :: Get BCode+getBDict = BDict . M.fromList <$> getWrapped 'd' 'e' (many getPairs)+ where getPairs = do+ (BString s) <- getBString+ x <- get+ return (s,x)++-- | Parses a BString+getBString :: Get BCode+getBString = do+ count <- getDigits+ BString <$> ( char ':' *> getStr (read count :: Integer))+ where maxInt = fromIntegral (maxBound :: Int) :: Integer+ + getStr n | n >= 0 = B.concat <$> (sequence $ getStr' n)+ | otherwise = fail $ "read a negative length string, length: " ++ show n+ + getStr' n | n > maxInt = getByteString maxBound : getStr' (n-maxInt)+ | otherwise = [getByteString . fromIntegral $ n]+++-- | Get one or more digit characters+getDigits :: Get String+getDigits = many1 digit++-- | Returns a character if it is a digit, fails otherwise. uses isDigit.+digit :: Get Char+digit = do+ x <- getCharG+ if isDigit x+ then return x+ else fail $ "Expected digit, got: " ++ show x+++-- * Put helper functions++-- | Put an element, wrapped by two characters+wrap :: Char -> Char -> Put -> Put+wrap a b m = do+ putWord8 (toW8 a)+ m+ putWord8 (toW8 b)++-- | Put something as it is shown using @show@+putShow :: Show a => a -> Put+putShow = mapM_ put . show++-- * Get Helper functions++-- | Parse zero or items using a given parser+many :: Get a -> Get [a]+many p = many1 p `mplus` return []++-- | Parse one or more items using a given parser+many1 :: Get a -> Get [a]+many1 p = (:) <$> p <*> many p++-- | Parse a given character+char :: Char -> Get Char+char c = do+ x <- getCharG+ if x == c+ then return c+ else fail $ "Expected char: '" ++ c:"' got: '" ++ [x,'\'']++-- | Get a Char. Only works with single byte characters+getCharG :: Get Char+getCharG = fromW8 <$> getWord8++-- BCode helper functions++-- | Return the hash of the info-dict in a torrent file+hashInfoDict :: BCode -> IO Digest+hashInfoDict bc =+ do ih <- case info bc of+ Nothing -> fail "Could not find infoHash"+ Just x -> return x+ let encoded = encode ih+ digest $ L.fromChunks $ [encoded]+++toPS :: String -> Path+toPS = PString . toBS++{- Simple search function over BCoded data structures, general case. In practice, we+ will prefer some simpler mnemonics -}+search :: [Path] -> BCode -> Maybe BCode+search [] bc = Just bc+search (PInt i : rest) (BArray bs) | i < 0 || i > length bs = Nothing+ | otherwise = search rest (bs!!i)+search (PString s : rest) (BDict mp) = M.lookup s mp >>= search rest+search _ _ = Nothing++search' :: String -> BCode -> Maybe B.ByteString+search' str b = case search [toPS str] b of+ Nothing -> Nothing+ Just (BString s) -> Just s+ _ -> Nothing++searchStr :: String -> BCode -> Maybe B.ByteString+searchStr = search'++searchInt :: String -> BCode -> Maybe Integer+searchInt str b = case search [toPS str] b of+ Just (BInt i) -> Just i+ _ -> Nothing++searchInfo :: String -> BCode -> Maybe BCode+searchInfo str = search [toPS "info", toPS str]++{- Various accessors -}+announce, comment, creationDate :: BCode -> Maybe B.ByteString+announce = search' "announce"+comment = search' "comment"+creationDate = search' "creation date"++{- Tracker accessors -}+trackerComplete, trackerIncomplete, trackerInterval :: BCode -> Maybe Integer+trackerMinInterval :: BCode -> Maybe Integer+trackerComplete = searchInt "complete"+trackerIncomplete = searchInt "incomplete"+trackerInterval = searchInt "interval"+trackerMinInterval = searchInt "min interval"++trackerError, trackerWarning :: BCode -> Maybe B.ByteString+trackerError = searchStr "failure reason"+trackerWarning = searchStr "warning mesage"++trackerPeers :: BCode -> Maybe B.ByteString+trackerPeers = searchStr "peers"++info :: BCode -> Maybe BCode+info = search [toPS "info"]++infoName :: BCode -> Maybe B.ByteString+infoName bc = case search [toPS "info", toPS "name"] bc of+ Just (BString s) -> Just s+ _ -> Nothing++infoPieceLength ::BCode -> Maybe Integer+infoPieceLength bc = do BInt i <- search [toPS "info", toPS "piece length"] bc+ return i++infoLength :: BCode -> Maybe Integer+infoLength bc = maybe length2 Just length1+ where+ -- |info/length key for single-file torrent+ length1 = do BInt i <- search [toPS "info", toPS "length"] bc+ return i+ -- |length summed from files of multi-file torrent+ length2 = sum `fmap`+ map snd `fmap`+ infoFiles bc++infoPieces :: BCode -> Maybe [B.ByteString]+infoPieces b = do t <- searchInfo "pieces" b+ case t of+ BString str -> return $ sha1Split str+ _ -> mzero+ where sha1Split r | r == B.empty = []+ | otherwise = block : sha1Split rest+ where (block, rest) = B.splitAt 20 r++numberPieces :: BCode -> Maybe Int+numberPieces = fmap length . infoPieces++infoFiles :: BCode -> Maybe [([String], Integer)] -- ^[(filePath, fileLength)]+infoFiles bc = let mbFpath = fromBS `fmap` infoName bc+ mbLength = infoLength bc+ mbFiles = do BArray fileList <- searchInfo "files" bc+ return $ do fileDict@(BDict _) <- fileList+ let Just (BInt length) = search [toPS "length"] fileDict+ Just (BArray path) = search [toPS "path"] fileDict+ path' = map (\(BString s) -> fromBS s) path+ return (path', length)+ in case (mbFpath, mbLength, mbFiles) of+ (Just fpath, _, Just files) ->+ Just $+ map (\(path, length) ->+ (fpath:path, length)+ ) files+ (Just fpath, Just length, _) ->+ Just [([fpath], length)]+ (_, _, Just files) ->+ Just files+ _ ->+ Nothing++pp :: BCode -> Doc+pp bc =+ case bc of+ BInt i -> integer i+ BString s -> text (show s)+ BArray arr -> text "[" <+> (cat $ intersperse comma al) <+> text "]"+ where al = map pp arr+ BDict mp -> text "{" <+> cat (intersperse comma mpl) <+> text "}"+ where mpl = map (\(s, bc') -> text (fromBS s) <+> text "->" <+> pp bc') $ M.toList mp++prettyPrint :: BCode -> String+prettyPrint = render . pp+++testDecodeEncodeProp1 :: BCode -> Bool+testDecodeEncodeProp1 m =+ let encoded = encode m+ decoded = decode encoded+ in case decoded of+ Left _ -> False+ Right m' -> m == m'++testData = [BInt 123,+ BInt (-123),+ BString (toBS "Hello"),+ BString (toBS ['\NUL'..'\255']),+ BArray [BInt 1234567890+ ,toBString "a longer string with eieldei stuff to mess things up"+ ],+ toBDict [+ ("hello",BInt 3)+ ,("a key",toBString "and a value")+ ,("a sub dict",toBDict [+ ("some stuff",BInt 1)+ ,("some more stuff", toBString "with a string")+ ])+ ]+ ]++toBDict :: [(String,BCode)] -> BCode+toBDict = BDict . M.fromList . map (\(k,v) -> ((toBS k),v))++toBString :: String -> BCode+toBString = BString . toBS++++
+ src/Protocol/Wire.hs view
@@ -0,0 +1,227 @@+{-+ A parser and encoder for the BitTorrent wire protocol using the+ cereal package.+-}+++module Protocol.Wire+where++import Control.Applicative hiding (empty)+import Control.Monad++import Data.Monoid+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as L++import Data.Serialize+import Data.Serialize.Put+import Data.Serialize.Get++import Data.Word+import Data.Char+import System.IO++import Logging+import Torrent++------------------------------------------------------------++type BitField = L.ByteString+type PieceLength = Int++data Message = KeepAlive+ | Choke+ | Unchoke+ | Interested+ | NotInterested+ | Have PieceNum -- Int+ | BitField BitField+ | Request PieceNum Block+ | Piece PieceNum Int B.ByteString+ | Cancel PieceNum Block+ | Port Integer+ deriving (Eq, Show)+++data HandShake = HandShake + String -- Protocol Header+ Word64 -- Extension Bias++-- | The Protocol header for the Peer Wire Protocol+protocolHeader :: String+protocolHeader = "BitTorrent protocol"++extensionBasis :: Word64+extensionBasis = 0++extensionFast :: Word64+extensionFast = 4++p8 :: Word8 -> Put+p8 = putWord8++p32be :: Integral a => a -> Put+p32be = putWord32be . fromIntegral++decodeMsg :: Get Message+decodeMsg = get++encodePacket :: Message -> B.ByteString+encodePacket m = mconcat [szEnc, mEnc]+ where mEnc = encode m+ sz = B.length mEnc+ szEnc = runPut . p32be $ sz++instance Serialize Message where+ put KeepAlive = return ()+ put Choke = p8 0+ put Unchoke = p8 1+ put Interested = p8 2+ put NotInterested = p8 3+ put (Have pn) = p8 4 *> p32be pn+ put (BitField bf) = p8 5 *> putLazyByteString bf+ put (Request pn (Block os sz))+ = p8 6 *> mapM_ p32be [pn,os,sz]+ put (Piece pn os c) = p8 7 *> mapM_ p32be [pn,os] *> putByteString c+ put (Cancel pn (Block os sz))+ = p8 8 *> mapM_ p32be [pn,os,sz]+ put (Port p) = p8 9 *> (putWord16be . fromIntegral $ p)++ get = getKA <|> getChoke+ <|> getUnchoke <|> getIntr+ <|> getNI <|> getHave+ <|> getBF <|> getReq+ <|> getPiece <|> getCancel+ <|> getPort++getChoke = byte 0 *> return Choke+getUnchoke = byte 1 *> return Unchoke+getIntr = byte 2 *> return Interested+getNI = byte 3 *> return NotInterested+getHave = byte 4 *> (Have <$> gw32)+getBF = byte 5 *> (BitField <$> (remaining >>= getLazyByteString . fromIntegral))+getReq = byte 6 *> (Request <$> gw32 <*> (Block <$> gw32 <*> gw32))+getPiece = byte 7 *> (Piece <$> gw32 <*> gw32 <*> (remaining >>= getByteString))+getCancel = byte 8 *> (Cancel <$> gw32 <*> (Block <$> gw32 <*> gw32))+getPort = byte 9 *> (Port . fromIntegral <$> getWord16be)+getKA = do+ empty <- isEmpty+ if empty+ then return KeepAlive+ else fail "Non empty message - not a KeepAlive"++gw32 :: Integral a => Get a+gw32 = fromIntegral <$> getWord32be++byte :: Word8 -> Get Word8+byte w = do+ x <- lookAhead getWord8+ if x == w+ then getWord8+ else fail $ "Expected byte: " ++ show w ++ " got: " ++ show x++-- | Size of the protocol header+protocolHeaderSize :: Int+protocolHeaderSize = length protocolHeader++-- | Protocol handshake code. This encodes the protocol handshake part+protocolHandshake :: L.ByteString+protocolHandshake = L.fromChunks . map runPut $+ [putWord8 $ fromIntegral protocolHeaderSize,+ mapM_ (putWord8 . fromIntegral . ord) protocolHeader,+ putWord64be extensionBasis]++toBS :: String -> B.ByteString+toBS = B.pack . map toW8++toLBS :: String -> L.ByteString+toLBS = L.pack . map toW8++toW8 :: Char -> Word8+toW8 = fromIntegral . ord++-- | Receive the header parts from the other end+receiveHeader :: Handle -> Int -> InfoHash+ -> IO (Either String ([Capabilities], L.ByteString))+receiveHeader h sz ih = parseHeader `fmap` B.hGet h sz+ where parseHeader = runGet (headerParser ih)++headerParser :: InfoHash -> Get ([Capabilities], L.ByteString)+headerParser ih = do+ hdSz <- getWord8+ when (fromIntegral hdSz /= protocolHeaderSize) $ fail "Wrong header size"+ protoString <- getByteString protocolHeaderSize+ when (protoString /= toBS protocolHeader) $ fail "Wrong protocol header"+ caps <- getWord64be+ ihR <- getLazyByteString 20+ when (ihR /= toLBS ih) $ fail "Wrong InfoHash"+ pid <- getLazyByteString 20+ return (decodeCapabilities caps, pid)+++data Capabilities = Fast+decodeCapabilities :: Word64 -> [Capabilities]+decodeCapabilities _ = []++-- | Initiate a handshake on a socket+initiateHandshake :: LogChannel -> Handle -> PeerId -> InfoHash+ -> IO (Either String ([Capabilities], L.ByteString))+initiateHandshake logC handle peerid infohash = do+ logMsg logC "Sending off handshake message"+ L.hPut handle msg+ hFlush handle+ logMsg logC "Receiving handshake from other end"+ receiveHeader handle sz infohash -- TODO: Exceptions+ where msg = L.fromChunks . map runPut $ [putLazyByteString protocolHandshake,+ putLazyByteString $ toLBS infohash,+ putByteString . toBS $ peerid]+ sz = fromIntegral (L.length msg)+-- +-- -- TESTS+testDecodeEncodeProp1 :: Message -> Bool+testDecodeEncodeProp1 m =+ let encoded = encode m+ decoded = decode encoded+ in case decoded of+ Left _ -> False+ Right m' -> m == m'++-- | The call @constructBitField pieces@ will return the a ByteString suitable for inclusion in a+-- BITFIELD message to a peer.+constructBitField :: Int -> [PieceNum] -> L.ByteString+constructBitField sz pieces = L.pack . build $ m+ where m = map (`elem` pieces) [0..sz-1 + pad]+ pad = 8 - (sz `mod` 8)+ build [] = []+ build l = let (first, rest) = splitAt 8 l+ in if length first /= 8+ then error "Wront bitfield"+ else bytify first : build rest+ bytify [b7,b6,b5,b4,b3,b2,b1,b0] = sum [if b0 then 1 else 0,+ if b1 then 2 else 0,+ if b2 then 4 else 0,+ if b3 then 8 else 0,+ if b4 then 16 else 0,+ if b5 then 32 else 0,+ if b6 then 64 else 0,+ if b7 then 128 else 0]++-- Prelude.map testDecodeEncodeProp1 +testData = [KeepAlive,+ Choke,+ Unchoke,+ Interested,+ NotInterested,+ Have 0,+ Have 1,+ Have 1934729,+ BitField (L.pack [1,2,3]),+ Request 123 (Block 4 7),+ Piece 5 7 (B.pack [1,2,3,4,5,6,7,8,9,0]),+ Piece 5 7 (B.pack (concat . replicate 30 $ [minBound..maxBound])),+ Cancel 5 (Block 6 7),+ Port 123+ ]+-- Currently returns all True+
+ src/RateCalc.hs view
@@ -0,0 +1,76 @@+-- | Rate calculation.+module RateCalc (+ -- * Types+ Rate+ -- * Interface+ , new+ , update+ , extractCount+ , extractRate+ )++where++import Data.Time.Clock++-- | A Rate is a record of information used for calculating the rate+data Rate = Rate+ { rate :: Double -- ^ The current rate+ , bytes :: Integer -- ^ The amount of bytes transferred since last rate extraction+ , count :: Integer -- ^ The amount of bytes transferred since last count extraction+ , nextExpected :: UTCTime -- ^ When is the next rate update expected+ , lastExt :: UTCTime -- ^ When was the last rate update+ , rateSince :: UTCTime -- ^ From where is the rate measured+ }++fudge :: NominalDiffTime+fudge = fromInteger 5 -- Seconds++rateUpdate :: Integer+rateUpdate = 5 * 1000 -- Millisecs++maxRatePeriod :: NominalDiffTime+maxRatePeriod = fromInteger 20 -- Seconds++new :: UTCTime -> Rate+new t = Rate { rate = 0.0+ , bytes = 0+ , count = 0+ , nextExpected = addUTCTime fudge t+ , lastExt = addUTCTime (-fudge) t+ , rateSince = addUTCTime (-fudge) t+ }++-- | The call @update n rt@ updates the rate structure @rt@ with @n@ new bytes+update :: Integer -> Rate -> Rate+update n rt = rt { bytes = bytes rt + n+ , count = count rt + n}++-- | The call @extractRate t rt@ extracts the current rate from the rate structure and updates the rate+-- structures internal book-keeping+extractRate :: UTCTime -> Rate -> (Double, Rate)+extractRate t rt =+ let oldWindow :: Double+ oldWindow = realToFrac $ diffUTCTime (lastExt rt) (rateSince rt)+ newWindow :: Double+ newWindow = realToFrac $ diffUTCTime t (rateSince rt)+ n = bytes rt+ r = (rate rt * oldWindow + (fromIntegral n)) / newWindow+ expectN = min 5 (round $ (fromIntegral n / (max r 0.0001)))+ in+ -- Update the rate and book-keep the missing pieces. The total is simply a built-in+ -- counter. The point where we expect the next update is pushed at most 5 seconds ahead+ -- in time. But it might come earlier if the rate is high.+ -- Last is updated with the current time. Finally, we move the windows earliest value+ -- forward if it is more than 20 seconds from now.+ (r, rt { rate = r+ , bytes = 0+ , nextExpected = addUTCTime (fromInteger expectN) t+ , lastExt = t+ , rateSince = max (rateSince rt) (addUTCTime (-maxRatePeriod) t)+ })++-- | The call @extractCount rt@ extract the bytes transferred since last extraction+extractCount :: Rate -> (Integer, Rate)+extractCount rt = (count rt, rt { count = 0 })+
+ src/Supervisor.hs view
@@ -0,0 +1,177 @@+-- | Erlang style supervisors for Haskell.+-- Note that yet, these are not really good enough for using in other projects.+-- are currently subject to change until I figure out how a nice interface will+-- look like. At that moment they could be split off into their own package.+{-# LANGUAGE ScopedTypeVariables #-}+module Supervisor (+ -- * Types+ Child(..)+ , Children+ , SupervisorMsg(..)+ , SupervisorChan+ -- * Supervisor Initialization+ , allForOne+ , oneForOne+ -- * helper calls+ , pDie+ , defaultStopHandler+ )+where++import Control.Applicative+import Control.Concurrent+import Control.Concurrent.CML+import Control.Monad.State+import Control.Monad.Reader++import Prelude hiding (catch)++import Logging+import Process++data Child = Supervisor (SupervisorChan -> IO ThreadId)+ | Worker (SupervisorChan -> IO ThreadId)++data SupervisorMsg = IAmDying ThreadId+ | PleaseDie ThreadId+ | SpawnNew Child++type SupervisorChan = Channel SupervisorMsg+type Children = [Child]++data ChildInfo = HSupervisor ThreadId+ | HWorker ThreadId+++pDie :: SupervisorChan -> IO ()+pDie supC = do+ tid <- myThreadId+ sync $ transmit supC $ IAmDying tid++class SupervisorConf a where+ getParent :: a -> SupervisorChan+ getChan :: a -> SupervisorChan++data CFOFA = CFOFA { name :: String+ , chan :: SupervisorChan+ , parent :: SupervisorChan+ , logCh :: LogChannel }++instance SupervisorConf CFOFA where+ getParent = parent+ getChan = chan++instance Logging CFOFA where+ getLogger cf = (name cf, logCh cf) -- TODO: Better naming!++data STOFA = STOFA { childInfo :: [ChildInfo] }++-- | Run a set of processes and do it once in the sense that if someone dies,+-- no restart is attempted. We will just kill off everybody without any kind+-- of prejudice.+allForOne :: String -> Children -> LogChannel -> SupervisorChan -> IO ThreadId+allForOne name children logC parentC = do+ c <- channel+ spawnP (CFOFA name c parentC logC) (STOFA []) (catchP startup+ (defaultStopHandler parentC))+ where+ startup = do+ childs <- mapM spawnChild children+ modify (\_ -> STOFA childs)+ forever eventLoop+ eventLoop = do+ mTid <- liftIO myThreadId+ syncP =<< chooseP [childEvent, parentEvent mTid]+ childEvent = do+ ev <- recvPC chan+ wrapP ev (\msg -> case msg of+ IAmDying tid -> do gets childInfo >>= mapM_ finChild+ t <- liftIO myThreadId+ sendPC parent (IAmDying t) >>= syncP+ SpawnNew chld -> do n <- spawnChild chld+ modify (\(STOFA cs) -> STOFA (n : cs)))+ parentEvent mTid = do+ ev <- recvP parentC (\m -> case m of+ PleaseDie tid | tid == mTid -> True+ _ -> False)+ wrapP ev (\msg -> case msg of+ PleaseDie _ -> gets childInfo >>= mapM_ finChild+ _ -> return ())++data CFOFO = CFOFO { oName :: String+ , oChan :: SupervisorChan+ , oparent :: SupervisorChan+ , ologCh :: LogChannel }++instance SupervisorConf CFOFO where+ getParent = oparent+ getChan = oChan++instance Logging CFOFO where+ getLogger cf = (oName cf, ologCh cf) -- TODO: Better naming!++data STOFO = STOFO [ChildInfo]++-- | A One-for-one supervisor is called with @oneForOne children parentCh@. It will spawn and run+-- @children@ and be linked into the supervisor structure on @parentCh@. It returns the ThreadId+-- of the supervisor itself and the Channel of which it is the controller.+--+-- Should a process die, the one-for-one supervisor will do nothing about it. It will just record+-- the death and let the other processes keep running.+--+-- TODO: Restart policies.+oneForOne :: String -> Children -> LogChannel -> SupervisorChan -> IO (ThreadId, SupervisorChan)+oneForOne name children logC parentC = do+ c <- channel+ t <- spawnP (CFOFO name c parentC logC) (STOFO []) (catchP startup+ (defaultStopHandler parentC))+ return (t, c)+ where+ startup :: Process CFOFO STOFO ()+ startup = do+ childs <- mapM spawnChild children+ modify (\_ -> STOFO childs)+ forever eventLoop+ eventLoop :: Process CFOFO STOFO ()+ eventLoop = do+ mTid <- liftIO myThreadId+ syncP =<< chooseP [childEvent, parentEvent mTid]+ childEvent = do+ ev <- recvPC oChan+ wrapP ev (\msg -> case msg of+ IAmDying tid -> pruneChild tid+ SpawnNew chld -> do n <- spawnChild chld+ modify (\(STOFO cs) -> STOFO (n : cs)))+ parentEvent mTid = do+ ev <- recvP parentC (\m -> case m of+ PleaseDie tid | tid == mTid -> True+ _ -> False)+ wrapP ev (\msg -> do (STOFO cs) <- get+ mapM_ finChild cs+ stopP)+ pruneChild tid = modify (\(STOFO cs) -> STOFO (filter check cs))+ where check (HSupervisor t) = t == tid+ check (HWorker t) = t == tid+++finChild :: SupervisorConf a => ChildInfo -> Process a b ()+finChild (HWorker tid) = liftIO $ killThread tid -- Make this call killP in Process?+finChild (HSupervisor tid) = do+ c <- getChan <$> ask+ syncP =<< sendP c (PleaseDie tid)++spawnChild :: SupervisorConf a => Child -> Process a b ChildInfo+spawnChild (Worker proc) = do+ c <- getChan <$> ask+ tid <- liftIO $ proc c+ return $ HWorker tid+spawnChild (Supervisor proc) = do+ c <- getChan <$> ask+ tid <- liftIO $ proc c+ return $ HSupervisor tid++defaultStopHandler :: SupervisorChan -> Process a b ()+defaultStopHandler supC = do+ t <- liftIO $ myThreadId+ syncP =<< (sendP supC $ IAmDying t)+
+ src/Torrent.hs view
@@ -0,0 +1,161 @@+-- Haskell Torrent+-- Copyright (c) 2009, Jesper Louis Andersen,+-- All rights reserved.+--+-- Redistribution and use in source and binary forms, with or without+-- modification, are permitted provided that the following conditions are+-- met:+--+-- * Redistributions of source code must retain the above copyright+-- notice, this list of conditions and the following disclaimer.+-- * Redistributions in binary form must reproduce the above copyright+-- notice, this list of conditions and the following disclaimer in the+-- documentation and/or other materials provided with the distribution.+--+-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS+-- IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,+-- THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR+-- PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR+-- CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,+-- EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,+-- PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR+-- PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF+-- LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING+-- NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS+-- SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.++-- | The following module is responsible for general types used+-- throughout the system.+module Torrent (+ -- * Types+ InfoHash+ , PeerId+ , AnnounceURL+ , TorrentState(..)+ , TorrentInfo(..)+ , PieceNum+ , PieceSize+ , PieceMap+ , PiecesDoneMap+ , PieceInfo(..)+ , BlockSize+ , Block(..)+ -- * Interface+ , determineState+ , bytesLeft+ , defaultBlockSize+ , defaultOptimisticSlots+ , defaultPort+ , haskellTorrentVersion+ , mkPeerId+ , mkTorrentInfo+ )+where++import qualified Data.Foldable as F+import qualified Data.ByteString as B+import Data.List+import qualified Data.Map as M++import Network+import Numeric++import System.Random++import Protocol.BCode+import Digest++-- | The type of Infohashes as used in torrents. These are identifiers+-- of torrents+type InfoHash = Digest++-- | The peerId is the ID of a client. It is used to identify clients+-- from each other+type PeerId = String++-- | The internal type of Announce URLs+type AnnounceURL = B.ByteString++-- | Internal type for a torrent. It identifies a torrent in various places of the system.+data TorrentInfo = TorrentInfo {+ infoHash :: InfoHash,+ pieceCount :: Int, -- Number of pieces in torrent+ announceURL :: AnnounceURL } deriving Show++data TorrentState = Seeding | Leeching++-- PIECES+----------------------------------------------------------------------+type PieceNum = Int+type PieceSize = Int++data PieceInfo = PieceInfo {+ offset :: Integer, -- ^ Offset of the piece, might be greater than Int+ len :: Integer, -- ^ Length of piece; usually a small value+ digest :: String -- ^ Digest of piece; taken from the .torret file+ } deriving (Eq, Show)++type PieceMap = M.Map PieceNum PieceInfo++-- | The PiecesDoneMap is a map which is true if we have the piece and false otherwise+type PiecesDoneMap = M.Map PieceNum Bool++-- | Given what pieces that are done, return the current state of the client.+determineState :: PiecesDoneMap -> TorrentState+determineState pd | F.all (==True) pd = Seeding+ | otherwise = Leeching++-- | Return the amount of bytes left on a torrent given what pieces are done and the+-- map of the shape of the torrent in question.+bytesLeft :: PiecesDoneMap -> PieceMap -> Integer+bytesLeft done pm =+ M.foldWithKey (\k v accu ->+ case M.lookup k done of+ Just False -> (len v) + accu+ _ -> accu) 0 pm++-- BLOCKS+----------------------------------------------------------------------+type BlockSize = Int++data Block = Block { blockOffset :: Int -- ^ offset of this block within the piece+ , blockSize :: BlockSize -- ^ size of this block within the piece+ } deriving (Eq, Ord, Show)++defaultBlockSize :: BlockSize+defaultBlockSize = 16384 -- Bytes++-- | Default number of optimistic slots+defaultOptimisticSlots :: Int+defaultOptimisticSlots = 2++-- | Default port to communicate on+defaultPort :: PortID+defaultPort = PortNumber $ fromInteger 1579++-- | The current version of Haskell-Torrent. It should be be here.+haskellTorrentVersion :: String+haskellTorrentVersion = "d001"++-- | Convert a BCode block into its corresponding TorrentInfo block, perhaps+-- failing in the process.+mkTorrentInfo :: BCode -> IO TorrentInfo+mkTorrentInfo bc = do+ (ann, np) <- case queryInfo bc of Nothing -> fail "Could not create torrent info"+ Just x -> return x+ ih <- hashInfoDict bc+ return TorrentInfo { infoHash = ih, announceURL = ann, pieceCount = np }+ where+ queryInfo bc =+ do ann <- announce bc+ np <- numberPieces bc+ return (ann, np)++-- | Create a new PeerId for this client+mkPeerId :: StdGen -> PeerId+mkPeerId gen = header ++ take (20 - length header) ranString+ where randomList :: Int -> StdGen -> [Int]+ randomList n = take n . unfoldr (Just . random)+ rs = randomList 10 gen+ ranString = concatMap (\i -> showHex (abs i) "") rs+ header = "-HT" ++ haskellTorrentVersion ++ "-"
+ src/Version.hs.in view
@@ -0,0 +1,8 @@+-- | Haskell-torrent version+module Version (version) where++githead :: String+githead = "@GITHEAD@"++version :: String+version = githead