diff options
author | Sam Truzjan <pxqr.sta@gmail.com> | 2014-02-08 07:19:31 +0400 |
---|---|---|
committer | Sam Truzjan <pxqr.sta@gmail.com> | 2014-02-08 07:19:31 +0400 |
commit | 7edacaaedd432c71169bbd59c9c0948e9a83da26 (patch) | |
tree | 1ca88d119c3575c502e184bfcc15572ee6a406f9 | |
parent | f1f28f1a128caa3df5cdab2eb4c22ec07633af06 (diff) |
Add multitracker session
-rw-r--r-- | bittorrent.cabal | 4 | ||||
-rw-r--r-- | src/Network/BitTorrent/Tracker/Cache.hs | 164 | ||||
-rw-r--r-- | src/Network/BitTorrent/Tracker/Session.hs | 206 | ||||
-rw-r--r-- | tests/Network/BitTorrent/Tracker/CacheSpec.hs | 7 | ||||
-rw-r--r-- | tests/Network/BitTorrent/Tracker/SessionSpec.hs | 25 |
5 files changed, 406 insertions, 0 deletions
diff --git a/bittorrent.cabal b/bittorrent.cabal index d0a3764a..415f2a69 100644 --- a/bittorrent.cabal +++ b/bittorrent.cabal | |||
@@ -74,11 +74,13 @@ library | |||
74 | -- Network.BitTorrent.Exchange.Session | 74 | -- Network.BitTorrent.Exchange.Session |
75 | Network.BitTorrent.Exchange.Status | 75 | Network.BitTorrent.Exchange.Status |
76 | Network.BitTorrent.Exchange.Wire | 76 | Network.BitTorrent.Exchange.Wire |
77 | Network.BitTorrent.Tracker.Cache | ||
77 | Network.BitTorrent.Tracker.List | 78 | Network.BitTorrent.Tracker.List |
78 | Network.BitTorrent.Tracker.Message | 79 | Network.BitTorrent.Tracker.Message |
79 | Network.BitTorrent.Tracker.RPC | 80 | Network.BitTorrent.Tracker.RPC |
80 | Network.BitTorrent.Tracker.RPC.HTTP | 81 | Network.BitTorrent.Tracker.RPC.HTTP |
81 | Network.BitTorrent.Tracker.RPC.UDP | 82 | Network.BitTorrent.Tracker.RPC.UDP |
83 | Network.BitTorrent.Tracker.Session | ||
82 | Network.BitTorrent.Tracker.Wai | 84 | Network.BitTorrent.Tracker.Wai |
83 | System.Torrent.FileMap | 85 | System.Torrent.FileMap |
84 | System.Torrent.Storage | 86 | System.Torrent.Storage |
@@ -188,11 +190,13 @@ test-suite spec | |||
188 | Network.BitTorrent.DHT.MessageSpec | 190 | Network.BitTorrent.DHT.MessageSpec |
189 | Network.BitTorrent.DHT.RoutingSpec | 191 | Network.BitTorrent.DHT.RoutingSpec |
190 | Network.BitTorrent.DHT.TokenSpec | 192 | Network.BitTorrent.DHT.TokenSpec |
193 | Network.BitTorrent.Tracker.CacheSpeŃ | ||
191 | Network.BitTorrent.Tracker.ListSpec | 194 | Network.BitTorrent.Tracker.ListSpec |
192 | Network.BitTorrent.Tracker.MessageSpec | 195 | Network.BitTorrent.Tracker.MessageSpec |
193 | Network.BitTorrent.Tracker.RPCSpec | 196 | Network.BitTorrent.Tracker.RPCSpec |
194 | Network.BitTorrent.Tracker.RPC.HTTPSpec | 197 | Network.BitTorrent.Tracker.RPC.HTTPSpec |
195 | Network.BitTorrent.Tracker.RPC.UDPSpec | 198 | Network.BitTorrent.Tracker.RPC.UDPSpec |
199 | Network.BitTorrent.Tracker.SessionSpec | ||
196 | Network.BitTorrent.Exchange.MessageSpec | 200 | Network.BitTorrent.Exchange.MessageSpec |
197 | System.Torrent.StorageSpec | 201 | System.Torrent.StorageSpec |
198 | System.Torrent.FileMapSpec | 202 | System.Torrent.FileMapSpec |
diff --git a/src/Network/BitTorrent/Tracker/Cache.hs b/src/Network/BitTorrent/Tracker/Cache.hs new file mode 100644 index 00000000..28a4adcb --- /dev/null +++ b/src/Network/BitTorrent/Tracker/Cache.hs | |||
@@ -0,0 +1,164 @@ | |||
1 | -- | | ||
2 | -- Copyright : (c) Sam Truzjan 2014 | ||
3 | -- License : BSD | ||
4 | -- Maintainer : pxqr.sta@gmail.com | ||
5 | -- Stability : experimental | ||
6 | -- Portability : portable | ||
7 | -- | ||
8 | -- Cached data for tracker responses. | ||
9 | -- | ||
10 | module Network.BitTorrent.Tracker.Cache | ||
11 | ( -- * Cache | ||
12 | Cached | ||
13 | , lastUpdated | ||
14 | , updateInterval | ||
15 | , minUpdateInterval | ||
16 | |||
17 | -- * Construction | ||
18 | , newCached | ||
19 | , newCached_ | ||
20 | |||
21 | -- * Query | ||
22 | , isAlive | ||
23 | , isStalled | ||
24 | , isExpired | ||
25 | , canUpdate | ||
26 | , shouldUpdate | ||
27 | |||
28 | -- * Cached data | ||
29 | , tryTakeData | ||
30 | , takeData | ||
31 | ) where | ||
32 | |||
33 | import Control.Applicative | ||
34 | import Data.Monoid | ||
35 | import Data.Default | ||
36 | import Data.Time | ||
37 | import Data.Time.Clock.POSIX | ||
38 | |||
39 | |||
40 | data Cached a = Cached | ||
41 | { -- | Time of resource creation. | ||
42 | lastUpdated :: !POSIXTime | ||
43 | |||
44 | -- | Minimum invalidation timeout. | ||
45 | , minUpdateInterval :: !NominalDiffTime | ||
46 | |||
47 | -- | Resource lifetime. | ||
48 | , updateInterval :: !NominalDiffTime | ||
49 | |||
50 | -- | Resource data. | ||
51 | , cachedData :: a | ||
52 | } deriving (Show, Eq) | ||
53 | |||
54 | -- INVARIANT: minUpdateInterval <= updateInterval | ||
55 | |||
56 | instance Default (Cached a) where | ||
57 | def = mempty | ||
58 | |||
59 | instance Functor Cached where | ||
60 | fmap f (Cached t i m a) = Cached t i m (f a) | ||
61 | |||
62 | posixEpoch :: NominalDiffTime | ||
63 | posixEpoch = 1000000000000000000000000000000000000000000000000000000 | ||
64 | |||
65 | instance Applicative Cached where | ||
66 | pure = Cached 0 posixEpoch posixEpoch | ||
67 | f <*> c = Cached | ||
68 | { lastUpdated = undefined | ||
69 | , minUpdateInterval = undefined | ||
70 | , updateInterval = undefined | ||
71 | , cachedData = cachedData f (cachedData c) | ||
72 | } | ||
73 | |||
74 | instance Alternative Cached where | ||
75 | empty = mempty | ||
76 | (<|>) = error "cached alternative instance: not implemented" | ||
77 | |||
78 | instance Monad Cached where | ||
79 | return = pure | ||
80 | Cached {..} >>= f = Cached | ||
81 | { lastUpdated = undefined | ||
82 | , updateInterval = undefined | ||
83 | , minUpdateInterval = undefined | ||
84 | , cachedData = undefined | ||
85 | } | ||
86 | |||
87 | instance Monoid (Cached a) where | ||
88 | mempty = Cached | ||
89 | { lastUpdated = 0 | ||
90 | , minUpdateInterval = 0 | ||
91 | , updateInterval = 0 | ||
92 | , cachedData = error "cached mempty: impossible happen" | ||
93 | } | ||
94 | |||
95 | mappend a b | ||
96 | | expirationTime a > expirationTime b = a | ||
97 | | otherwise = b | ||
98 | |||
99 | normalize :: NominalDiffTime -> NominalDiffTime | ||
100 | -> (NominalDiffTime, NominalDiffTime) | ||
101 | normalize a b | ||
102 | | a < b = (a, b) | ||
103 | | otherwise = (b, a) | ||
104 | {-# INLINE normalize #-} | ||
105 | |||
106 | newCached :: NominalDiffTime -> NominalDiffTime -> a -> IO (Cached a) | ||
107 | newCached minInterval interval x = do | ||
108 | t <- getPOSIXTime | ||
109 | let (mui, ui) = normalize minInterval interval | ||
110 | return Cached | ||
111 | { lastUpdated = t | ||
112 | , minUpdateInterval = mui | ||
113 | , updateInterval = ui | ||
114 | , cachedData = x | ||
115 | } | ||
116 | |||
117 | newCached_ :: NominalDiffTime -> a -> IO (Cached a) | ||
118 | newCached_ interval x = newCached interval interval x | ||
119 | {-# INLINE newCached_ #-} | ||
120 | |||
121 | expirationTime :: Cached a -> POSIXTime | ||
122 | expirationTime Cached {..} = undefined | ||
123 | |||
124 | isAlive :: Cached a -> IO Bool | ||
125 | isAlive Cached {..} = do | ||
126 | currentTime <- getPOSIXTime | ||
127 | return $ lastUpdated + updateInterval > currentTime | ||
128 | |||
129 | isExpired :: Cached a -> IO Bool | ||
130 | isExpired Cached {..} = undefined | ||
131 | |||
132 | isStalled :: Cached a -> IO Bool | ||
133 | isStalled Cached {..} = undefined | ||
134 | |||
135 | canUpdate :: Cached a -> IO (Maybe NominalDiffTime) | ||
136 | canUpdate = undefined --isStaled | ||
137 | |||
138 | shouldUpdate :: Cached a -> IO (Maybe NominalDiffTime) | ||
139 | shouldUpdate = undefined -- isExpired | ||
140 | |||
141 | tryTakeData :: Cached a -> IO (Maybe a) | ||
142 | tryTakeData c = do | ||
143 | alive <- isAlive c | ||
144 | return $ if alive then Just (cachedData c) else Nothing | ||
145 | |||
146 | invalidateData :: Cached a -> IO a -> IO (Cached a) | ||
147 | invalidateData Cached {..} action = do | ||
148 | t <- getPOSIXTime | ||
149 | x <- action | ||
150 | return Cached | ||
151 | { lastUpdated = t | ||
152 | , updateInterval = updateInterval | ||
153 | , minUpdateInterval = minUpdateInterval | ||
154 | , cachedData = x | ||
155 | } | ||
156 | |||
157 | takeData :: Cached a -> IO a -> IO a | ||
158 | takeData c action = do | ||
159 | mdata <- tryTakeData c | ||
160 | case mdata of | ||
161 | Just a -> return a | ||
162 | Nothing -> do | ||
163 | c' <- invalidateData c action | ||
164 | takeData c' action | ||
diff --git a/src/Network/BitTorrent/Tracker/Session.hs b/src/Network/BitTorrent/Tracker/Session.hs new file mode 100644 index 00000000..7be16fd6 --- /dev/null +++ b/src/Network/BitTorrent/Tracker/Session.hs | |||
@@ -0,0 +1,206 @@ | |||
1 | -- | | ||
2 | -- Copyright : (c) Sam Truzjan 2014 | ||
3 | -- License : BSD | ||
4 | -- Maintainer : pxqr.sta@gmail.com | ||
5 | -- Stability : experimental | ||
6 | -- Portability : portable | ||
7 | -- | ||
8 | -- Multitracker sessions. | ||
9 | -- | ||
10 | module Network.BitTorrent.Tracker.Session | ||
11 | ( -- * Session | ||
12 | Session | ||
13 | , newSession | ||
14 | , closeSession | ||
15 | |||
16 | -- * Events | ||
17 | , Event (..) | ||
18 | , notify | ||
19 | |||
20 | -- * Query | ||
21 | , askPeers | ||
22 | ) where | ||
23 | |||
24 | import Control.Applicative | ||
25 | import Control.Concurrent | ||
26 | import Control.Concurrent.STM | ||
27 | import Control.Exception | ||
28 | import Control.Monad | ||
29 | import Data.Default | ||
30 | import Data.Fixed | ||
31 | import Data.Foldable | ||
32 | import Data.List as L | ||
33 | import Data.Maybe | ||
34 | import Data.IORef | ||
35 | import Data.Text as T | ||
36 | import Data.Time | ||
37 | import Data.Traversable | ||
38 | import Network | ||
39 | import Network.URI | ||
40 | |||
41 | import Data.Torrent | ||
42 | import Data.Torrent.InfoHash | ||
43 | import Network.BitTorrent.Core | ||
44 | import Network.BitTorrent.Tracker.Cache | ||
45 | import Network.BitTorrent.Tracker.List | ||
46 | import Network.BitTorrent.Tracker.Message | ||
47 | import Network.BitTorrent.Tracker.RPC as RPC | ||
48 | |||
49 | {----------------------------------------------------------------------- | ||
50 | -- Tracker entry | ||
51 | -----------------------------------------------------------------------} | ||
52 | |||
53 | data Scrape = Scrape | ||
54 | { leechersCount :: Maybe Int | ||
55 | , seedersCount :: Maybe Int | ||
56 | } deriving (Show, Eq) | ||
57 | |||
58 | instance Default Scrape where | ||
59 | def = Scrape Nothing Nothing | ||
60 | |||
61 | |||
62 | data Status | ||
63 | = Running | ||
64 | | Paused | ||
65 | deriving (Show, Eq) | ||
66 | |||
67 | instance Default Status where | ||
68 | def = Paused | ||
69 | |||
70 | nextStatus :: Maybe Event -> Status | ||
71 | nextStatus Nothing = Running | ||
72 | nextStatus (Just Started ) = Running | ||
73 | nextStatus (Just Stopped ) = Paused | ||
74 | nextStatus (Just Completed) = Running | ||
75 | |||
76 | needNotify :: Maybe Event -> Maybe Status -> Bool | ||
77 | -- we always send _regular_ announce requests (for e.g. to get more peers); | ||
78 | needNotify Nothing _ = True | ||
79 | needNotify (Just Started) Nothing = True | ||
80 | needNotify (Just Stopped) Nothing = False | ||
81 | needNotify (Just Completed) Nothing = False | ||
82 | needNotify Nothing (Just Running) = True | ||
83 | needNotify Nothing (Just Paused ) = True | ||
84 | |||
85 | -- | Do we need to sent this event to a first working tracker or to | ||
86 | -- the all known good trackers? | ||
87 | allNotify :: Maybe Event -> Bool | ||
88 | allNotify Nothing = False | ||
89 | allNotify (Just Started) = False | ||
90 | allNotify (Just Stopped) = True | ||
91 | allNotify (Just Completed) = True | ||
92 | |||
93 | -- | Single tracker session. | ||
94 | data TrackerEntry = TrackerEntry | ||
95 | { -- | Tracker announce URI. | ||
96 | trackerURI :: !URI | ||
97 | |||
98 | -- | Used to notify 'Stopped' and 'Completed' events. | ||
99 | , statusSent :: !(Maybe Status) | ||
100 | |||
101 | -- | | ||
102 | , peersCache :: Cached [PeerAddr IP] | ||
103 | |||
104 | -- | May be used to show brief swarm stats in client GUI. | ||
105 | , scrapeCache :: Cached Scrape | ||
106 | } | ||
107 | |||
108 | nullEntry :: URI -> TrackerEntry | ||
109 | nullEntry uri = TrackerEntry uri Nothing def def | ||
110 | |||
111 | {----------------------------------------------------------------------- | ||
112 | -- Multitracker Session | ||
113 | -----------------------------------------------------------------------} | ||
114 | |||
115 | -- | Multitracker session. | ||
116 | data Session = Session | ||
117 | { infohash :: !InfoHash | ||
118 | , currentStatus :: !(MVar Status) | ||
119 | , trackers :: !(MVar (TrackerList TrackerEntry)) | ||
120 | } | ||
121 | |||
122 | -- Just Started | ||
123 | newSession :: InfoHash -> TrackerList URI -> IO Session | ||
124 | newSession ih origUris = do | ||
125 | uris <- shuffleTiers origUris | ||
126 | status <- newMVar def | ||
127 | entries <- newMVar (fmap nullEntry uris) | ||
128 | return (Session ih status entries) | ||
129 | |||
130 | -- Just Stopped | ||
131 | closeSession :: Session -> IO () | ||
132 | closeSession _ = return () | ||
133 | |||
134 | seconds :: Int -> NominalDiffTime | ||
135 | seconds n = realToFrac (toEnum n :: Uni) | ||
136 | |||
137 | cachePeers :: AnnounceInfo -> IO (Cached [PeerAddr IP]) | ||
138 | cachePeers AnnounceInfo {..} = | ||
139 | newCached (seconds respInterval) | ||
140 | (seconds (fromMaybe respInterval respMinInterval)) | ||
141 | (getPeerList respPeers) | ||
142 | |||
143 | cacheScrape :: AnnounceInfo -> IO (Cached Scrape) | ||
144 | cacheScrape AnnounceInfo {..} = | ||
145 | newCached (seconds respInterval) | ||
146 | (seconds (fromMaybe respInterval respMinInterval)) | ||
147 | Scrape | ||
148 | { seedersCount = respComplete | ||
149 | , leechersCount = respIncomplete | ||
150 | } | ||
151 | |||
152 | announceAll :: Manager -> Session -> Maybe Event -> IO () | ||
153 | announceAll mgr Session {..} mevent = do | ||
154 | modifyMVar_ trackers (traversal announceTo) | ||
155 | where | ||
156 | traversal | ||
157 | | allNotify mevent = traverseAll | ||
158 | | otherwise = traverseTiers | ||
159 | |||
160 | announceTo entry @ TrackerEntry {..} | ||
161 | | mevent `needNotify` statusSent = do | ||
162 | let q = SAnnounceQuery infohash def Nothing mevent | ||
163 | res <- RPC.announce mgr trackerURI q | ||
164 | TrackerEntry trackerURI (Just (nextStatus mevent)) | ||
165 | <$> cachePeers res <*> cacheScrape res | ||
166 | | otherwise = return entry | ||
167 | |||
168 | -- TODO send notifications to tracker periodically. | ||
169 | -- | | ||
170 | -- | ||
171 | -- This function /may/ block until tracker query proceed. | ||
172 | notify :: Manager -> Session -> Event -> IO () | ||
173 | notify mgr ses event = announceAll mgr ses (Just event) | ||
174 | |||
175 | -- TODO fork thread for reannounces | ||
176 | -- | | ||
177 | announce :: Manager -> Session -> IO () | ||
178 | announce mgr ses = announceAll mgr ses Nothing | ||
179 | |||
180 | -- TODO run announce if sesion have no peers | ||
181 | -- | This function /may/ block. Use async if needed. | ||
182 | askPeers :: Manager -> Session -> IO [PeerAddr IP] | ||
183 | askPeers mgr ses = do | ||
184 | list <- readMVar (trackers ses) | ||
185 | L.concat <$> collect (tryTakeData . peersCache) list | ||
186 | |||
187 | collect :: (a -> IO (Maybe b)) -> TrackerList a -> IO [b] | ||
188 | collect f lst =(catMaybes . toList) <$> traverse f lst | ||
189 | |||
190 | --sourcePeers :: Session -> Source (PeerAddr IP) | ||
191 | --sourcePeers | ||
192 | |||
193 | {----------------------------------------------------------------------- | ||
194 | -- State query | ||
195 | -----------------------------------------------------------------------} | ||
196 | |||
197 | data TrackerInfo = TrackerInfo | ||
198 | { | ||
199 | } | ||
200 | |||
201 | --instance ToJSON TrackerInfo where | ||
202 | -- toJSON = undefined | ||
203 | |||
204 | -- | | ||
205 | --getSessionState :: Session -> IO (TrackerList TrackerInfo) | ||
206 | --getSessionState = undefined | ||
diff --git a/tests/Network/BitTorrent/Tracker/CacheSpec.hs b/tests/Network/BitTorrent/Tracker/CacheSpec.hs new file mode 100644 index 00000000..db015957 --- /dev/null +++ b/tests/Network/BitTorrent/Tracker/CacheSpec.hs | |||
@@ -0,0 +1,7 @@ | |||
1 | module Network.BitTorrent.Tracker.CacheSpec (spec) where | ||
2 | import Test.Hspec | ||
3 | |||
4 | spec :: Spec | ||
5 | spec = do | ||
6 | describe "Cached" $ do | ||
7 | return () | ||
diff --git a/tests/Network/BitTorrent/Tracker/SessionSpec.hs b/tests/Network/BitTorrent/Tracker/SessionSpec.hs new file mode 100644 index 00000000..0c75fcaa --- /dev/null +++ b/tests/Network/BitTorrent/Tracker/SessionSpec.hs | |||
@@ -0,0 +1,25 @@ | |||
1 | module Network.BitTorrent.Tracker.SessionSpec (spec) where | ||
2 | import Data.Default | ||
3 | import Data.List as L | ||
4 | import Network.URI | ||
5 | import Test.Hspec | ||
6 | |||
7 | import Data.Torrent | ||
8 | import Network.BitTorrent.Tracker.List | ||
9 | import Network.BitTorrent.Tracker.RPC.UDPSpec (trackerURIs) | ||
10 | import Network.BitTorrent.Tracker.RPC | ||
11 | import Network.BitTorrent.Tracker.Session | ||
12 | |||
13 | |||
14 | trackers :: TrackerList URI | ||
15 | trackers = trackerList def { tAnnounceList = Just [trackerURIs] } | ||
16 | |||
17 | spec :: Spec | ||
18 | spec = do | ||
19 | describe "Session" $ do | ||
20 | it "" $ do | ||
21 | withManager def def $ \ m -> do | ||
22 | s <- newSession def trackers | ||
23 | notify m s Started | ||
24 | peers <- askPeers m s | ||
25 | peers `shouldSatisfy` (not . L.null) \ No newline at end of file | ||