summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSam Truzjan <pxqr.sta@gmail.com>2013-10-14 05:11:46 +0400
committerSam Truzjan <pxqr.sta@gmail.com>2013-10-14 05:11:46 +0400
commitb5f222ba7dfa1fa53b8b53f4e1b770193bb55fe4 (patch)
treef24b2f677d78a05139b9e190a60ea75bc64f49b4
parent9737a06bff6c6539a6afd67f7970a6923b401d86 (diff)
Move some modules from torrent-content
-rw-r--r--.ghci2
-rw-r--r--src/Data/Torrent/Bitfield.hs359
-rw-r--r--src/Network/BitTorrent/DHT/Protocol.hs6
-rw-r--r--src/Network/BitTorrent/Discovery.hs4
-rw-r--r--src/System/IO/MMap/Fixed.hs212
-rw-r--r--src/System/Torrent/Storage.hs336
-rw-r--r--tests/Main.hs80
7 files changed, 995 insertions, 4 deletions
diff --git a/.ghci b/.ghci
index 09a3d5f6..c5d93fc0 100644
--- a/.ghci
+++ b/.ghci
@@ -3,3 +3,5 @@ import Data.Serialize as S
3import Network 3import Network
4import Network.Socket hiding (send, sendTo, recv, recvFrom) 4import Network.Socket hiding (send, sendTo, recv, recvFrom)
5import Network.Socket.ByteString 5import Network.Socket.ByteString
6
7import Network.BitTorrent.DHT.Protocol \ No newline at end of file
diff --git a/src/Data/Torrent/Bitfield.hs b/src/Data/Torrent/Bitfield.hs
new file mode 100644
index 00000000..f7e036d5
--- /dev/null
+++ b/src/Data/Torrent/Bitfield.hs
@@ -0,0 +1,359 @@
1-- |
2-- Copyright : (c) Sam T. 2013
3-- License : MIT
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- This modules provides all necessary machinery to work with
9-- bitfields. Bitfields are used to keep track indices of complete
10-- pieces either peer have or client have.
11--
12-- There are also commonly used piece seletion algorithms
13-- which used to find out which one next piece to download.
14-- Selectors considered to be used in the following order:
15--
16-- * Random first - at the start.
17--
18-- * Rarest first selection - performed to avoid situation when
19-- rarest piece is unaccessible.
20--
21-- * _End game_ seletion - performed after a peer has requested all
22-- the subpieces of the content.
23--
24-- Note that BitTorrent applies the strict priority policy for
25-- /subpiece/ or /blocks/ selection.
26--
27{-# LANGUAGE CPP #-}
28{-# LANGUAGE BangPatterns #-}
29{-# LANGUAGE RecordWildCards #-}
30module Data.Torrent.Bitfield
31 ( PieceIx, PieceCount, Bitfield
32
33 -- * Construction
34 , haveAll, haveNone, have, singleton
35 , interval
36 , adjustSize
37
38 -- * Query
39 , Data.Torrent.Bitfield.null
40 , haveCount, totalCount, completeness
41
42 , member, notMember
43 , findMin, findMax
44
45 , isSubsetOf
46
47 , Frequency, frequencies, rarest
48
49 -- * Combine
50 , union
51 , intersection
52 , difference
53
54 -- * Serialization
55 , fromBitmap, toBitmap
56 , toList
57
58 -- * Selection
59 , Selector
60 , selector, strategyClass
61
62 , strictFirst, strictLast
63 , rarestFirst, randomFirst, endGame
64
65#if defined (TESTING)
66 -- * Debug
67 , mkBitfield
68#endif
69 ) where
70
71import Control.Monad
72import Control.Monad.ST
73import Data.ByteString (ByteString)
74import qualified Data.ByteString as B
75import qualified Data.ByteString.Lazy as Lazy
76import Data.Vector.Unboxed (Vector)
77import qualified Data.Vector.Unboxed as V
78import qualified Data.Vector.Unboxed.Mutable as VM
79import Data.IntervalSet (IntSet)
80import qualified Data.IntervalSet as S
81import qualified Data.IntervalSet.ByteString as S
82import Data.List (foldl')
83import Data.Monoid
84import Data.Ratio
85
86
87-- | Pieces indexed from zero up to 'PieceCount' value.
88type PieceIx = Int
89
90-- | Used to represent max set bound. Min set bound is always set to
91-- zero.
92type PieceCount = Int
93
94-- TODO cache some operations
95
96-- | Bitfields are represented just as integer sets but with
97-- restriction: the each set should be within given interval (or
98-- subset of the specified interval). Size is used to specify
99-- interval, so bitfield of size 10 might contain only indices in
100-- interval [0..9].
101--
102data Bitfield = Bitfield {
103 bfSize :: !PieceCount
104 , bfSet :: !IntSet
105 } deriving (Show, Read, Eq)
106
107-- Invariants: all elements of bfSet lie in [0..bfSize - 1];
108
109instance Monoid Bitfield where
110 {-# SPECIALIZE instance Monoid Bitfield #-}
111 mempty = haveNone 0
112 mappend = union
113 mconcat = unions
114
115{-----------------------------------------------------------------------
116 Construction
117-----------------------------------------------------------------------}
118
119-- | The empty bitfield of the given size.
120haveNone :: PieceCount -> Bitfield
121haveNone s = Bitfield s S.empty
122
123-- | The full bitfield containing all piece indices for the given size.
124haveAll :: PieceCount -> Bitfield
125haveAll s = Bitfield s (S.interval 0 (s - 1))
126
127-- | Insert the index in the set ignoring out of range indices.
128have :: PieceIx -> Bitfield -> Bitfield
129have ix Bitfield {..}
130 | 0 <= ix && ix < bfSize = Bitfield bfSize (S.insert ix bfSet)
131 | otherwise = Bitfield bfSize bfSet
132
133singleton :: PieceIx -> PieceCount -> Bitfield
134singleton ix pc = have ix (haveNone pc)
135
136-- | Assign new size to bitfield. FIXME Normally, size should be only
137-- decreased, otherwise exception raised.
138adjustSize :: PieceCount -> Bitfield -> Bitfield
139adjustSize s Bitfield {..} = Bitfield s bfSet
140
141-- | NOTE: for internal use only
142interval :: PieceCount -> PieceIx -> PieceIx -> Bitfield
143interval pc a b = Bitfield pc (S.interval a b)
144
145{-----------------------------------------------------------------------
146 Query
147-----------------------------------------------------------------------}
148
149-- | Test if bitifield have no one index: peer do not have anything.
150null :: Bitfield -> Bool
151null Bitfield {..} = S.null bfSet
152
153-- | Count of peer have pieces.
154haveCount :: Bitfield -> PieceCount
155haveCount = S.size . bfSet
156
157-- | Total count of pieces and its indices.
158totalCount :: Bitfield -> PieceCount
159totalCount = bfSize
160
161-- | Ratio of /have/ piece count to the /total/ piece count.
162--
163-- > forall bf. 0 <= completeness bf <= 1
164--
165completeness :: Bitfield -> Ratio PieceCount
166completeness b = haveCount b % totalCount b
167
168inRange :: PieceIx -> Bitfield -> Bool
169inRange ix Bitfield {..} = 0 <= ix && ix < bfSize
170
171member :: PieceIx -> Bitfield -> Bool
172member ix bf @ Bitfield {..}
173 | ix `inRange` bf = ix `S.member` bfSet
174 | otherwise = False
175
176notMember :: PieceIx -> Bitfield -> Bool
177notMember ix bf @ Bitfield {..}
178 | ix `inRange` bf = ix `S.notMember` bfSet
179 | otherwise = True
180
181-- | Find first available piece index.
182findMin :: Bitfield -> PieceIx
183findMin = S.findMin . bfSet
184{-# INLINE findMin #-}
185
186-- | Find last available piece index.
187findMax :: Bitfield -> PieceIx
188findMax = S.findMax . bfSet
189{-# INLINE findMax #-}
190
191isSubsetOf :: Bitfield -> Bitfield -> Bool
192isSubsetOf a b = bfSet a `S.isSubsetOf` bfSet b
193
194-- | Frequencies are needed in piece selection startegies which use
195-- availability quantity to find out the optimal next piece index to
196-- download.
197type Frequency = Int
198
199-- | How many times each piece index occur in the given bitfield set.
200frequencies :: [Bitfield] -> Vector Frequency
201frequencies [] = V.fromList []
202frequencies xs = runST $ do
203 v <- VM.new size
204 VM.set v 0
205 forM_ xs $ \ Bitfield {..} -> do
206 forM_ (S.toList bfSet) $ \ x -> do
207 fr <- VM.read v x
208 VM.write v x (succ fr)
209 V.unsafeFreeze v
210 where
211 size = maximum (map bfSize xs)
212
213-- TODO it seems like this operation is veeery slow
214
215-- | Find least available piece index. If no piece available return
216-- 'Nothing'.
217rarest :: [Bitfield] -> Maybe PieceIx
218rarest xs
219 | V.null freqMap = Nothing
220 | otherwise
221 = Just $ fst $ V.ifoldr' minIx (0, freqMap V.! 0) freqMap
222 where
223 freqMap = frequencies xs
224
225 minIx :: PieceIx -> Frequency
226 -> (PieceIx, Frequency)
227 -> (PieceIx, Frequency)
228 minIx ix fr acc@(_, fra)
229 | fr < fra && fr > 0 = (ix, fr)
230 | otherwise = acc
231
232
233{-----------------------------------------------------------------------
234 Combine
235-----------------------------------------------------------------------}
236
237-- | Find indices at least one peer have.
238union :: Bitfield -> Bitfield -> Bitfield
239union a b = {-# SCC union #-} Bitfield {
240 bfSize = bfSize a `max` bfSize b
241 , bfSet = bfSet a `S.union` bfSet b
242 }
243
244-- | Find indices both peers have.
245intersection :: Bitfield -> Bitfield -> Bitfield
246intersection a b = {-# SCC intersection #-} Bitfield {
247 bfSize = bfSize a `min` bfSize b
248 , bfSet = bfSet a `S.intersection` bfSet b
249 }
250
251-- | Find indices which have first peer but do not have the second peer.
252difference :: Bitfield -> Bitfield -> Bitfield
253difference a b = {-# SCC difference #-} Bitfield {
254 bfSize = bfSize a -- FIXME is it reasonable?
255 , bfSet = bfSet a `S.difference` bfSet b
256 }
257
258-- | Find indices the any of the peers have.
259unions :: [Bitfield] -> Bitfield
260unions = {-# SCC unions #-} foldl' union (haveNone 0)
261
262{-----------------------------------------------------------------------
263 Serialization
264-----------------------------------------------------------------------}
265
266-- | List all have indexes.
267toList :: Bitfield -> [PieceIx]
268toList Bitfield {..} = S.toList bfSet
269
270-- | Unpack 'Bitfield' from tightly packed bit array. Note resulting
271-- size might be more than real bitfield size, use 'adjustSize'.
272fromBitmap :: ByteString -> Bitfield
273fromBitmap bs = {-# SCC fromBitmap #-} Bitfield {
274 bfSize = B.length bs * 8
275 , bfSet = S.fromByteString bs
276 }
277{-# INLINE fromBitmap #-}
278
279-- | Pack a 'Bitfield' to tightly packed bit array.
280toBitmap :: Bitfield -> Lazy.ByteString
281toBitmap Bitfield {..} = {-# SCC toBitmap #-} Lazy.fromChunks [intsetBM, alignment]
282 where
283 byteSize = bfSize `div` 8 + if bfSize `mod` 8 == 0 then 0 else 1
284 alignment = B.replicate (byteSize - B.length intsetBM) 0
285 intsetBM = S.toByteString bfSet
286
287{-----------------------------------------------------------------------
288 Debug
289-----------------------------------------------------------------------}
290
291-- | For internal use only.
292mkBitfield :: PieceCount -> [PieceIx] -> Bitfield
293mkBitfield s ixs = Bitfield {
294 bfSize = s
295 , bfSet = S.splitGT (-1) $ S.splitLT s $ S.fromList ixs
296 }
297
298{-----------------------------------------------------------------------
299 Selection
300-----------------------------------------------------------------------}
301
302type Selector = Bitfield -- ^ Indices of client /have/ pieces.
303 -> Bitfield -- ^ Indices of peer /have/ pieces.
304 -> [Bitfield] -- ^ Indices of other peers /have/ pieces.
305 -> Maybe PieceIx -- ^ Zero-based index of piece to request
306 -- to, if any.
307
308selector :: Selector -- ^ Selector to use at the start.
309 -> Ratio PieceCount
310 -> Selector -- ^ Selector to use after the client have
311 -- the C pieces.
312 -> Selector -- ^ Selector that changes behaviour based
313 -- on completeness.
314selector start pt ready h a xs =
315 case strategyClass pt h of
316 SCBeginning -> start h a xs
317 SCReady -> ready h a xs
318 SCEnd -> endGame h a xs
319
320data StartegyClass
321 = SCBeginning
322 | SCReady
323 | SCEnd
324 deriving (Show, Eq, Ord, Enum, Bounded)
325
326
327strategyClass :: Ratio PieceCount -> Bitfield -> StartegyClass
328strategyClass threshold = classify . completeness
329 where
330 classify c
331 | c < threshold = SCBeginning
332 | c + 1 % numerator c < 1 = SCReady
333 -- FIXME numerator have is not total count
334 | otherwise = SCEnd
335
336
337-- | Select the first available piece.
338strictFirst :: Selector
339strictFirst h a _ = Just $ findMin (difference a h)
340
341-- | Select the last available piece.
342strictLast :: Selector
343strictLast h a _ = Just $ findMax (difference a h)
344
345-- |
346rarestFirst :: Selector
347rarestFirst h a xs = rarest (map (intersection want) xs)
348 where
349 want = difference h a
350
351-- | In average random first is faster than rarest first strategy but
352-- only if all pieces are available.
353randomFirst :: Selector
354randomFirst = do
355-- randomIO
356 error "randomFirst"
357
358endGame :: Selector
359endGame = strictLast
diff --git a/src/Network/BitTorrent/DHT/Protocol.hs b/src/Network/BitTorrent/DHT/Protocol.hs
index 73e5fa68..0ac814b7 100644
--- a/src/Network/BitTorrent/DHT/Protocol.hs
+++ b/src/Network/BitTorrent/DHT/Protocol.hs
@@ -42,7 +42,7 @@ import Network.BitTorrent.Exchange.Protocol ()
42 42
43type NodeId = ByteString 43type NodeId = ByteString
44 44
45-- WARN is the 'system' random suitable for this? 45-- TODO WARN is the 'system' random suitable for this?
46-- | Generate random NodeID used for the entire session. 46-- | Generate random NodeID used for the entire session.
47-- Distribution of ID's should be as uniform as possible. 47-- Distribution of ID's should be as uniform as possible.
48-- 48--
@@ -56,9 +56,7 @@ data NodeAddr = NodeAddr {
56 56
57instance Serialize NodeAddr where 57instance Serialize NodeAddr where
58 get = NodeAddr <$> getWord32be <*> get 58 get = NodeAddr <$> getWord32be <*> get
59 put NodeAddr {..} = do 59 put NodeAddr {..} = putWord32be nodeIP >> put nodePort
60 putWord32be nodeIP
61 put nodePort
62 60
63data NodeInfo = NodeInfo { 61data NodeInfo = NodeInfo {
64 nodeID :: !NodeId 62 nodeID :: !NodeId
diff --git a/src/Network/BitTorrent/Discovery.hs b/src/Network/BitTorrent/Discovery.hs
new file mode 100644
index 00000000..f26864a7
--- /dev/null
+++ b/src/Network/BitTorrent/Discovery.hs
@@ -0,0 +1,4 @@
1type PeerCount = Int
2
3defaultChanSize :: PeerCount
4defaultChanSize = defaultNumWant * 2
diff --git a/src/System/IO/MMap/Fixed.hs b/src/System/IO/MMap/Fixed.hs
new file mode 100644
index 00000000..1e83c350
--- /dev/null
+++ b/src/System/IO/MMap/Fixed.hs
@@ -0,0 +1,212 @@
1-- TODO pprint
2-- TODO see if this IntervalMap is overkill: Interval dataty have 4 constrs
3-- TODO clarify lifetime in docs
4-- TODO use madvise
5-- TODO unmap selected interval
6-- TODO tests
7-- TODO benchmarks
8-- TODO unmap overlapped regions
9-- [A] TODO lazy mapping for 32 bit arch;
10-- we need tricky algorithm and a lot of perf tests
11-- TODO use memmove in write bytes
12-- TODO write elem, write byte, read byte
13-- |
14-- Copyright : (c) Sam T. 2013
15-- License : MIT
16-- Maintainer : pxqr.sta@gmail.com
17-- Stability : experimental
18-- Portability : portable
19--
20-- This library provides mechanism to mmap files to fixed address
21-- with fine-grained control. Hovewer, instead of using MAP_FIXED we
22-- create our own address space upon virtual address space. If you
23-- would like you could call this space as "fixed address space".
24--
25-- This solves a few problems:
26--
27-- * Page already in use. If you mmap one file at 0..x addresses and
28-- want to map second file to x..y addresses using MAP_FIXED you
29-- can get in troubles: page might be mapped already. Raw call to
30-- mmap will silently unmap x..y addresses and then mmap our second
31-- file. So here we have extra unmap we would like to avoid.
32--
33-- * Page boundaries. If you mmap one file at x..x+1 you could
34-- not map next file to say addresses x+1..x+2.
35--
36-- Internally we make ordinary call to mmap to map a file and then
37-- using /interval map/ we map fixed address space to virtual
38-- address space. It takes TODO time in TODO cases.
39--
40-- Basically this library could be used when we need coalesce
41-- several files in arbitrary way. We could map at any position as
42-- long as offset + size fit in 'Int'.
43--
44-- For other details see:
45--
46-- > http://hackage.haskell.org/package/mmap
47-- > man mmap
48--
49{-# LANGUAGE RecordWildCards #-}
50module System.IO.MMap.Fixed
51 ( -- * Intervals
52 FixedOffset, FileOffset, FixedInterval, FileInterval
53 , interval, fileInterval
54
55 -- * Construction
56 , Fixed, Bytes
57 , System.IO.MMap.Fixed.empty, insertTo
58 , coalesceFiles
59
60 -- ** Specialized 'insertTo'
61 , mmapTo, mallocTo
62 , lookupRegion
63
64 -- * Query
65 , upperAddr
66
67 -- * Access
68 , viewBytes, readBytes, writeBytes
69 , readElem, writeElem
70 ) where
71
72import Data.ByteString.Lazy as Lazy
73import Data.ByteString.Lazy.Internal as Lazy
74import Data.ByteString.Internal as B
75import Data.List as L
76import Data.Int
77import Data.IntervalMap.Strict as M
78import Data.IntervalMap.Interval
79import System.IO.MMap
80import Foreign
81
82
83type FixedOffset = Int
84type FileOffset = Int64
85type Size = Int
86
87
88type FileInterval = (FileOffset, Size)
89type FixedInterval = Interval FixedOffset
90
91
92interval :: FixedOffset -> Size -> FixedInterval
93interval off s = IntervalCO off (off + fromIntegral (max 0 s))
94{-# INLINE interval #-}
95
96fileInterval :: FileOffset -> Size -> FileInterval
97fileInterval off s = (off, s)
98{-# INLINE fileInterval #-}
99
100intervalSize :: FixedInterval -> Size
101intervalSize i = upperBound i - lowerBound i
102{-# INLINE intervalSize #-}
103
104
105type Bytes = (ForeignPtr Word8, Size)
106
107type FixedMap = IntervalMap FixedOffset Bytes
108
109newtype Fixed = Fixed { imap :: FixedMap }
110
111instance Show Fixed where
112 show = show . M.toList . imap
113
114
115mapIM :: (FixedMap -> FixedMap) -> Fixed -> Fixed
116mapIM f s = s { imap = f (imap s) }
117
118empty :: Fixed
119empty = Fixed M.empty
120
121coalesceFiles :: [(FilePath, Int)] -> IO Fixed
122coalesceFiles = go 0 System.IO.MMap.Fixed.empty
123 where
124 go _ s [] = return s
125 go offset s ((path, bsize) : xs) = do
126 s' <- mmapTo path (0, bsize) offset s
127 go (offset + bsize) s' xs
128
129upperAddr :: Fixed -> FixedOffset
130upperAddr = upperBound . fst . findLast . imap
131
132insertTo :: FixedInterval -> Bytes -> Fixed -> Fixed
133insertTo fi mm = mapIM (M.insert fi mm)
134{-# INLINE insertTo #-}
135
136mmapTo :: FilePath -> FileInterval -> FixedOffset -> Fixed -> IO Fixed
137mmapTo path mrange to s = do
138 (fptr, offset, fsize) <- mmapFileForeignPtr path ReadWriteEx (Just mrange)
139
140 let fixed = interval to fsize
141 let mmaped = (fptr, offset)
142
143 return $ insertTo fixed mmaped s
144
145mallocTo :: FixedInterval -> Fixed -> IO Fixed
146mallocTo fi s = do
147 let bsize = intervalSize fi
148 fptr <- mallocForeignPtrBytes bsize
149 return (insertTo fi (fptr, 0) s)
150
151lookupRegion :: FixedOffset -> Fixed -> Maybe B.ByteString
152lookupRegion offset Fixed {..} =
153 case intersecting imap $ IntervalCO offset (succ offset) of
154 [(i, (fptr, off))] -> let s = upperBound i - lowerBound i
155 in Just $ fromForeignPtr fptr off (max 0 s)
156 _ -> Nothing
157
158-- | Note: this is unsafe operation.
159viewBytes :: FixedInterval -> Fixed -> Lazy.ByteString
160viewBytes fi s = fromChunks $ L.map mk $ (imap s `intersecting` fi)
161 where
162 mk (i, (fptr, offset)) =
163 let dropB = max 0 (lowerBound fi - lowerBound i)
164 dropT = max 0 (upperBound i - upperBound fi)
165 bsize = intervalSize i - (dropT + dropB)
166 in fromForeignPtr fptr (offset + dropB) bsize
167
168
169readBytes :: FixedInterval -> Fixed -> IO Lazy.ByteString
170readBytes fi s = let c = Lazy.copy (viewBytes fi s) in mkCopy c >> return c
171{-# INLINE readBytes #-}
172
173writeBytes :: FixedInterval -> Lazy.ByteString -> Fixed -> IO ()
174writeBytes fi bs s = bscpy (viewBytes fi s) bs
175{-# INLINE writeBytes #-}
176
177-- | Note: this operation takes O(log(files count)) time, if possible
178-- use readBytes.
179readElem :: Storable a => Fixed -> FixedOffset -> IO a
180readElem s offset = go undefined
181 where
182 go :: Storable a => a -> IO a
183 go dont_touch = do
184 let bsize = sizeOf dont_touch
185 let PS fptr off _ = Lazy.toStrict (viewBytes (interval offset bsize) s)
186 withForeignPtr fptr $ \ ptr -> peekByteOff ptr off
187
188writeElem :: Storable a => Fixed -> FixedOffset -> a -> IO ()
189writeElem s offset x = do
190 let bsize = sizeOf x
191 let PS fptr off _ = Lazy.toStrict (viewBytes (interval offset bsize) s)
192 withForeignPtr fptr $ \ptr -> pokeByteOff ptr off x
193
194
195mkCopy :: Lazy.ByteString -> IO ()
196mkCopy Empty = return ()
197mkCopy (Chunk _ x) = mkCopy x
198
199bscpy :: Lazy.ByteString -> Lazy.ByteString -> IO ()
200bscpy (PS _ _ 0 `Chunk` dest_rest) src = bscpy dest_rest src
201bscpy dest (PS _ _ 0 `Chunk` src_rest) = bscpy dest src_rest
202bscpy (PS dest_fptr dest_off dest_size `Chunk` dest_rest)
203 (PS src_fptr src_off src_size `Chunk` src_rest)
204 = do let csize = min dest_size src_size
205 withForeignPtr dest_fptr $ \dest_ptr ->
206 withForeignPtr src_fptr $ \src_ptr ->
207 memcpy (dest_ptr `advancePtr` dest_off)
208 (src_ptr `advancePtr` src_off)
209 (fromIntegral csize) -- TODO memmove?
210 bscpy (PS dest_fptr (dest_off + csize) (dest_size - csize) `Chunk` dest_rest)
211 (PS src_fptr (src_off + csize) (src_size - csize) `Chunk` src_rest)
212bscpy _ _ = return () \ No newline at end of file
diff --git a/src/System/Torrent/Storage.hs b/src/System/Torrent/Storage.hs
new file mode 100644
index 00000000..16f888bf
--- /dev/null
+++ b/src/System/Torrent/Storage.hs
@@ -0,0 +1,336 @@
1-- |
2-- Copyright : (c) Sam T. 2013
3-- License : MIT
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : non-portable
7--
8-- This module implements mapping from single continious block space
9-- to file storage. Storage can be used in two modes:
10--
11-- * As in memory storage - in this case we don't touch filesystem.
12--
13-- * As ordinary mmaped file storage - when we need to store
14-- data in the filesystem.
15--
16{-# LANGUAGE DoAndIfThenElse #-}
17{-# LANGUAGE NamedFieldPuns #-}
18{-# LANGUAGE RecordWildCards #-}
19module System.Torrent.Storage
20 ( Storage (metainfo)
21 , ppStorage
22
23 -- * Construction
24 , openStorage, closeStorage, withStorage
25 , getCompleteBitfield
26
27 -- * Modification
28 , getBlk, putBlk, selBlk
29 , getPiece, validatePiece
30
31 -- * TODO expose only File interface!
32 -- * File interface
33 , FD
34 , openFD, flushFD, closeFD
35 , readFD, writeFD
36 ) where
37
38import Control.Applicative
39import Control.Concurrent.STM
40import Control.Exception
41import Control.Monad
42import Control.Monad.Trans
43
44import Data.ByteString as B
45import qualified Data.ByteString.Lazy as Lazy
46import Text.PrettyPrint
47import System.FilePath
48import System.Directory
49import Foreign.C.Error
50
51import Data.Torrent.Bitfield as BF
52import Data.Torrent.Block
53import Data.Torrent.Metainfo
54import System.IO.MMap.Fixed as Fixed
55
56-- TODO merge piece validation and Sessions.available into one transaction.
57data Storage = Storage {
58 -- |
59 metainfo :: !Torrent
60
61 -- | Bitmask of complete and verified _pieces_.
62 , complete :: !(TVar Bitfield)
63
64 -- | Bitmask of complete _blocks_.
65 , blocks :: !(TVar Bitfield)
66 -- TODO use bytestring for fast serialization
67 -- because we need to write this bitmap to disc periodically
68
69 , blockSize :: !Int
70
71 -- | Used to map linear block addresses to disjoint
72 -- mallocated/mmaped adresses.
73 , payload :: !Fixed
74 }
75
76ppStorage :: Storage -> IO Doc
77ppStorage Storage {..} = pp <$> readTVarIO blocks
78 where
79 pp bf = int blockSize
80
81getCompleteBitfield :: Storage -> STM Bitfield
82getCompleteBitfield Storage {..} = readTVar complete
83
84{-----------------------------------------------------------------------
85 Construction
86-----------------------------------------------------------------------}
87
88-- TODO doc args
89openStorage :: Torrent -> FilePath -> Bitfield -> IO Storage
90openStorage t @ Torrent {..} contentPath bf = do
91 let content_paths = contentLayout contentPath tInfo
92 mapM_ (mkDir . fst) content_paths
93
94 let blockSize = defaultBlockSize `min` ciPieceLength tInfo
95 Storage t <$> newTVarIO bf
96 <*> newTVarIO (haveNone (blockCount blockSize tInfo))
97 <*> pure blockSize
98 <*> coalesceFiles content_paths
99 where
100 mkDir path = do
101 let dirPath = fst (splitFileName path)
102 exist <- doesDirectoryExist dirPath
103 unless exist $ do
104 createDirectoryIfMissing True dirPath
105
106-- TODO
107closeStorage :: Storage -> IO ()
108closeStorage st = return ()
109
110
111withStorage :: Torrent -> FilePath -> Bitfield -> (Storage -> IO a) -> IO a
112withStorage se path bf = bracket (openStorage se path bf) closeStorage
113
114{-----------------------------------------------------------------------
115 Modification
116-----------------------------------------------------------------------}
117
118-- TODO to avoid races we might need to try Control.Concurrent.yield
119-- TODO make block_payload :: Lazy.ByteString
120
121selBlk :: MonadIO m => PieceIx -> Storage -> m [BlockIx]
122selBlk pix st @ Storage {..}
123 = liftIO $ {-# SCC selBlk #-} atomically $ do
124 mask <- pieceMask pix st
125 select mask <$> readTVar blocks
126 where
127 select mask = fmap mkBix . toList . difference mask
128 -- TODO clip upper bound of block index
129 mkBix ix = BlockIx pix (blockSize * (ix - offset)) blockSize
130
131 offset = coeff * pix
132 coeff = ciPieceLength (tInfo metainfo) `div` blockSize
133
134--
135-- TODO make global lock map -- otherwise we might get broken pieces
136--
137-- imagine the following situation:
138--
139-- thread1: write
140-- thread1: mark
141--
142-- this let us avoid races as well
143--
144
145-- | Write a block to the storage. If block out of range then block is clipped.
146--
147--
148--
149putBlk :: MonadIO m => Block -> Storage -> m Bool
150putBlk blk @ Block {..} st @ Storage {..}
151 = liftIO $ {-# SCC putBlk #-} do
152-- let blkIx = undefined
153-- bm <- readTVarIO blocks
154-- unless (member blkIx bm) $ do
155 writeBytes (blkInterval (ciPieceLength (tInfo metainfo)) blk) blkData payload
156
157 markBlock blk st
158 completePiece blkPiece st
159
160markBlock :: Block -> Storage -> IO ()
161markBlock Block {..} Storage {..} = {-# SCC markBlock #-} do
162 let piLen = ciPieceLength (tInfo metainfo)
163 let glIx = (piLen `div` blockSize) * blkPiece + (blkOffset `div` blockSize)
164 atomically $ modifyTVar' blocks (have glIx)
165
166-- | Read a block by given block index. If lower or upper bound out of
167-- range then index is clipped.
168--
169-- Do not block.
170--
171getBlk :: MonadIO m => BlockIx -> Storage -> m Block
172getBlk ix @ BlockIx {..} st @ Storage {..}
173 = liftIO $ {-# SCC getBlk #-} do
174 -- TODO check if __piece__ is available
175 let piLen = ciPieceLength (tInfo metainfo)
176 bs <- readBytes (ixInterval piLen ix) payload
177 return $ Block ixPiece ixOffset bs
178
179getPiece :: PieceIx -> Storage -> IO Lazy.ByteString
180getPiece pix st @ Storage {..} = {-# SCC getPiece #-} do
181 let piLen = ciPieceLength (tInfo metainfo)
182 let bix = BlockIx pix 0 piLen
183 let bs = viewBytes (ixInterval piLen bix) payload
184 return $ bs
185
186resetPiece :: PieceIx -> Storage -> IO ()
187resetPiece pix st @ Storage {..}
188 = {-# SCC resetPiece #-} atomically $ do
189 mask <- pieceMask pix st
190 modifyTVar' blocks (`difference` mask)
191
192validatePiece :: Storage -> PieceIx -> IO Bool
193validatePiece storage pix = do
194 checkPiece (tInfo (metainfo storage)) pix <$> getPiece pix storage
195
196completePiece :: PieceIx -> Storage -> IO Bool
197completePiece pix st @ Storage {..} = {-# SCC completePiece #-} do
198 downloaded <- atomically $ isDownloaded pix st
199 if not downloaded then return False
200 else do
201 piece <- getPiece pix st
202 if checkPiece (tInfo metainfo) pix piece
203 then do
204 atomically $ modifyTVar' complete (BF.have pix)
205 return True
206 else do
207 print $ "----------------------------- invalid " ++ show pix
208-- resetPiece pix st
209 return True
210
211-- | Check each piece in the storage against content info hash.
212--
213-- Note that this function will block until each the entire storage
214-- checked. This may take a long time for a big torrents ­ use fork
215-- if needed.
216--
217validateStorage :: Storage -> IO ()
218validateStorage st = undefined -- (`validatePiece` st) [0..pieceCount st]
219
220{-----------------------------------------------------------------------
221 POSIX-like file interface
222------------------------------------------------------------------------
223This is useful for virtual filesystem writers and just for per file
224interface.
225-----------------------------------------------------------------------}
226-- TODO reference counting: storage might be closed before all FDs
227-- gets closed!
228-- or we can forbid to close storage and use finalizers only?
229
230type Offset = Int
231type Size = Int
232
233data FD = FD {
234 fdData :: ByteString
235 , fdNoBlock :: Bool
236 }
237
238
239-- TODO return "is dir" error
240-- | This call correspond to open(2) with the following parameters:
241--
242-- * OpenMode = ReadOnly;
243--
244-- * OpenFileFlags = O_NONBLOCK. (not true yet)
245--
246openFD :: FilePath -> Bool -> Storage -> IO (Either Errno FD)
247openFD path nonblock Storage {..}
248 | Just offset <- fileOffset path (tInfo metainfo)
249 , Just bs <- lookupRegion (fromIntegral offset) payload
250 = return $ Right $ FD bs nonblock
251 | otherwise = return $ Left $ eNOENT
252
253-- | Cancel all enqueued read operations and report any delayed
254-- errors.
255flushFD :: FD -> IO Errno
256flushFD _ = return eOK
257
258-- | This call correspond to close(2).
259closeFD :: FD -> IO ()
260closeFD _ = return ()
261
262-- TODO
263maskRegion :: FD -> Offset -> Size -> Maybe Size
264maskRegion FD {..} offset siz = return siz
265
266-- TODO
267isComplete :: FD -> Offset -> Size -> IO Size
268isComplete _ _ siz = return siz
269
270-- TODO
271enqueueRead :: FD -> Offset -> Size -> IO ()
272enqueueRead _ _ _ = return ()
273
274-- TODO
275readAhead :: FD -> Offset -> Size -> IO ()
276readAhead _ _ _ = return ()
277
278-- TODO
279waitRegion :: FD -> Offset -> Size -> IO ByteString
280waitRegion _ _ _ = return B.empty
281
282-- TODO implement blocking and non blocking modes?
283-- TODO check if region completely downloaded
284-- TODO if not we could return EAGAIN
285-- TODO enqueue read to piece manager
286-- | This call correspond to pread(2).
287readFD :: FD -> Offset -> Size -> IO (Either Errno ByteString)
288readFD fd @ FD {..} offset reqSize =
289 case maskRegion fd offset reqSize of
290 Nothing -> return $ Right B.empty
291 Just expSize -> do
292 availSize <- isComplete fd offset expSize
293 if availSize == expSize then haveAllReg expSize else haveSomeReg expSize
294 where
295 haveAllReg expSize = do
296 readAhead fd offset expSize
297 return $ Right $ slice offset expSize fdData
298
299 haveSomeReg expSize
300 | fdNoBlock = return $ Left $ eAGAIN
301 | otherwise = do
302 bs <- waitRegion fd offset expSize
303 readAhead fd offset expSize
304 return $ Right bs
305
306-- TODO implement COW; needed for applications which want to change files.
307writeFD :: FD -> ByteString -> Offset -> IO ()
308writeFD FD {..} bs offset = return ()
309
310{-----------------------------------------------------------------------
311 Internal
312-----------------------------------------------------------------------}
313
314isDownloaded :: PieceIx -> Storage -> STM Bool
315isDownloaded pix st @ Storage {..} = do
316 bf <- readTVar blocks
317 mask <- pieceMask pix st
318 return $ intersection mask bf == mask
319
320pieceMask :: PieceIx -> Storage -> STM Bitfield
321pieceMask pix Storage {..} = do
322 bf <- readTVar blocks
323 return $ BF.interval (totalCount bf) offset (offset + coeff - 1)
324 where
325 offset = coeff * pix
326 coeff = ciPieceLength (tInfo metainfo) `div` blockSize
327
328
329ixInterval :: Int -> BlockIx -> FixedInterval
330ixInterval pieceSize BlockIx {..} =
331 Fixed.interval (ixPiece * pieceSize + ixOffset) ixLength
332
333blkInterval :: Int -> Block -> FixedInterval
334blkInterval pieceSize Block {..} =
335 Fixed.interval (blkPiece * pieceSize + blkOffset)
336 (fromIntegral (Lazy.length blkData)) \ No newline at end of file
diff --git a/tests/Main.hs b/tests/Main.hs
index fb69565d..6180069f 100644
--- a/tests/Main.hs
+++ b/tests/Main.hs
@@ -59,6 +59,86 @@ instance Arbitrary URI where
59instance Arbitrary Text where 59instance Arbitrary Text where
60 arbitrary = T.pack <$> arbitrary 60 arbitrary = T.pack <$> arbitrary
61 61
62{-
63{-----------------------------------------------------------------------
64 MemMap
65-----------------------------------------------------------------------}
66
67tmpdir :: FilePath
68tmpdir = "tmp"
69
70boundaryTest :: Assertion
71boundaryTest = do
72 f <- mallocTo (Fixed.interval 0 1) Fixed.empty
73 f <- mallocTo (Fixed.interval 1 2) f
74 writeElem f 0 (1 :: Word8)
75 writeElem f 1 (2 :: Word8)
76 bs <- readBytes (Fixed.interval 0 2) f
77 "\x1\x2" @=? bs
78
79mmapSingle :: Assertion
80mmapSingle = do
81 f <- mmapTo (tmpdir </> "single.test") (10, 5) 5 Fixed.empty
82 writeBytes (Fixed.interval 5 5) "abcde" f
83 bs <- readBytes (Fixed.interval 5 5) f
84 "abcde" @=? bs
85
86coalesceTest :: Assertion
87coalesceTest = do
88 f <- mmapTo (tmpdir </> "a.test") (0, 1) 10 Fixed.empty
89 f <- mmapTo (tmpdir </> "bc.test") (0, 2) 12 f
90 f <- mmapTo (tmpdir </> "c.test") (0, 1) 13 f
91 writeBytes (Fixed.interval 10 4) "abcd" f
92 bs <- readBytes (Fixed.interval 10 4) f
93 "abcd" @=? bs
94-}
95
96{-----------------------------------------------------------------------
97 Bitfield
98-----------------------------------------------------------------------}
99-- other properties are tested in IntervalSet
100
101prop_completenessRange :: Bitfield -> Bool
102prop_completenessRange bf = 0 <= c && c <= 1
103 where
104 c = completeness bf
105
106prop_minMax :: Bitfield -> Bool
107prop_minMax bf
108 | BF.null bf = True
109 | otherwise = BF.findMin bf <= BF.findMax bf
110
111prop_rarestInRange :: [Bitfield] -> Bool
112prop_rarestInRange xs = case rarest xs of
113 Just r -> 0 <= r
114 && r < totalCount (maximumBy (comparing totalCount) xs)
115 Nothing -> True
116
117{- this one should give pretty good coverage -}
118prop_differenceDeMorgan :: Bitfield -> Bitfield -> Bitfield -> Bool
119prop_differenceDeMorgan a b c =
120 (a `BF.difference` (b `BF.intersection` c))
121 == ((a `BF.difference` b) `BF.union` (a `BF.difference` c))
122 &&
123 (a `BF.difference` (b `BF.union` c))
124 == ((a `BF.difference` b) `BF.intersection` (a `BF.difference` c))
125
126 {-
127 [ -- mem map
128 testCase "boudary" boundaryTest
129 , testCase "single" mmapSingle
130 , testCase "coalesce" coalesceTest
131 ]
132
133instance Arbitrary Bitfield where
134 arbitrary = mkBitfield <$> (succ . min 1000 <$> positive)
135 <*> arbitrary
136
137 testProperty "completeness range" prop_completenessRange
138 , testProperty "rarest in range" prop_rarestInRange
139 , testProperty "min less that max" prop_minMax
140 , testProperty "difference de morgan" prop_differenceDeMorgan
141-}
62{----------------------------------------------------------------------- 142{-----------------------------------------------------------------------
63 Handshake 143 Handshake
64-----------------------------------------------------------------------} 144-----------------------------------------------------------------------}