instrumentation: add next-share/
[cs-p2p-next.git] / instrumentation / next-share / BaseLib / Core / DownloadState.py
1 # Written by Arno Bakker 
2 # see LICENSE.txt for license information
3 """ Contains a snapshot of the state of the Download at a specific point in time. """
4 import time
5
6 import sys
7 from traceback import print_exc,print_stack
8
9 from BaseLib.Core.simpledefs import *
10 from BaseLib.Core.defaults import *
11 from BaseLib.Core.exceptions import *
12 from BaseLib.Core.Base import *
13 from BaseLib.Core.DecentralizedTracking.repex import REPEX_SWARMCACHE_SIZE
14
15 DEBUG = False
16
17 class DownloadState(Serializable):
18     """
19     Contains a snapshot of the state of the Download at a specific
20     point in time. Using a snapshot instead of providing live data and 
21     protecting access via locking should be faster.
22     
23     cf. libtorrent torrent_status
24     """
25     def __init__(self,download,status,error,progress,stats=None,filepieceranges=None,logmsgs=None,coopdl_helpers=[],coopdl_coordinator=None,peerid=None,videoinfo=None,swarmcache=None):
26         """ Internal constructor.
27         @param download The Download this state belongs too.
28         @param status The status of the Download (DLSTATUS_*)
29         @param progress The general progress of the Download.
30         @param stats The BT engine statistics for the Download.
31         @param filepieceranges The range of pieces that we are interested in. 
32         The get_pieces_complete() returns only completeness information about 
33         this range. This is used for playing a video in a multi-torrent file.
34         @param logmsgs A list of messages from the BT engine which may be of 
35         """
36         # Raynor Vliegendhart, TODO: documentation of DownloadState seems incomplete?
37         # RePEX: @param swarmcache The latest SwarmCache known by Download. This
38         #        cache will be used when the download is not running.
39         # RePEX TODO: instead of being passed the latest SwarmCache, DownloadState could
40         # also query it from Download? Perhaps add get_swarmcache to Download(Impl)?
41         
42         self.download = download
43         self.filepieceranges = filepieceranges # NEED CONC CONTROL IF selected_files RUNTIME SETABLE
44         self.logmsgs = logmsgs
45         self.coopdl_helpers = coopdl_helpers
46         self.coopdl_coordinator = coopdl_coordinator
47         
48         # RePEX: stored swarmcache from Download and store current time
49         if swarmcache is not None:
50             self.swarmcache = dict(swarmcache)
51         else:
52             self.swarmcache = None
53         self.time = time.time()
54         
55         if stats is None:
56             # No info available yet from download engine
57             self.error = error # readonly access
58             self.progress = progress
59             if self.error is not None:
60                 self.status = DLSTATUS_STOPPED_ON_ERROR
61             else:
62                 self.status = status
63             self.stats = None
64         elif error is not None:
65             self.error = error # readonly access
66             self.progress = 0.0 # really want old progress
67             self.status = DLSTATUS_STOPPED_ON_ERROR
68             self.stats = None
69         elif status is not None and status != DLSTATUS_REPEXING:
70             # For HASHCHECKING and WAITING4HASHCHECK
71             self.error = error
72             self.status = status
73             if self.status == DLSTATUS_WAITING4HASHCHECK:
74                 self.progress = 0.0
75             else:
76                 self.progress = stats['frac']
77             self.stats = None
78         else:
79             # Copy info from stats
80             self.error = None
81             self.progress = stats['frac']
82             if stats['frac'] == 1.0:
83                 self.status = DLSTATUS_SEEDING
84             else:
85                 self.status = DLSTATUS_DOWNLOADING
86             #print >>sys.stderr,"STATS IS",stats
87             
88             # Safe to store the stats dict. The stats dict is created per
89             # invocation of the BT1Download returned statsfunc and contains no
90             # pointers.
91             #
92             self.stats = stats
93             
94             # for pieces complete
95             statsobj = self.stats['stats']
96             if self.filepieceranges is None:
97                 self.haveslice = statsobj.have # is copy of network engine list
98             else:
99                 # Show only pieces complete for the selected ranges of files
100                 totalpieces =0
101                 for t,tl,f in self.filepieceranges:
102                     diff = tl-t
103                     totalpieces += diff
104                     
105                 #print >>sys.stderr,"DownloadState: get_pieces_complete",totalpieces
106                 
107                 haveslice = [False] * totalpieces
108                 haveall = True
109                 index = 0
110                 for t,tl,f in self.filepieceranges:
111                     for piece in range(t,tl):
112                         haveslice[index] = statsobj.have[piece]
113                         if haveall and haveslice[index] == False:
114                             haveall = False
115                         index += 1 
116                 self.haveslice = haveslice
117                 if haveall and len(self.filepieceranges) > 0:
118                     # we have all pieces of the selected files
119                     self.status = DLSTATUS_SEEDING
120                     self.progress = 1.0
121             
122             # RePEX: REPEXING status overrides SEEDING/DOWNLOADING status.
123             if status is not None and status == DLSTATUS_REPEXING:
124                 self.status = DLSTATUS_REPEXING
125             
126
127     def get_download(self):
128         """ Returns the Download object of which this is the state """
129         return self.download
130     
131     def get_progress(self):
132         """ The general progress of the Download as a percentage. When status is 
133          * DLSTATUS_HASHCHECKING it is the percentage of already downloaded 
134            content checked for integrity.
135          * DLSTATUS_DOWNLOADING/SEEDING it is the percentage downloaded.
136         @return Progress as a float (0..1).
137         """
138         return self.progress
139         
140     def get_status(self):
141         """ Returns the status of the torrent.
142         @return DLSTATUS_* """
143         return self.status
144
145     def get_error(self):
146         """ Returns the Exception that caused the download to be moved to 
147         DLSTATUS_STOPPED_ON_ERROR status.
148         @return Exception
149         """
150         return self.error
151
152     #
153     # Details
154     # 
155     def get_current_speed(self,direct):
156         """
157         Returns the current up or download speed.
158         @return The speed in KB/s, as float.
159         """
160         if self.stats is None:
161             return 0.0
162         if direct == UPLOAD:
163             return self.stats['up']/1024.0
164         else:
165             return self.stats['down']/1024.0
166
167     def get_total_transferred(self,direct):
168         """
169         Returns the total amount of up or downloaded bytes.
170         @return The amount in bytes.
171         """
172         if self.stats is None:
173             return 0L
174         # self.stats:          BitTornado.BT1.DownloaderFeedback.py (return from gather method)
175         # self.stats["stats"]: BitTornado.BT1.Statistics.py (Statistics_Response instance)
176         if direct == UPLOAD:
177             return self.stats['stats'].upTotal
178         else:
179             return self.stats['stats'].downTotal
180     
181     def get_eta(self):
182         """
183         Returns the estimated time to finish of download.
184         @return The time in ?, as ?.
185         """
186         if self.stats is None:
187             return 0.0
188         else:
189             return self.stats['time']
190         
191     def get_num_peers(self):
192         """ 
193         Returns the download's number of active connections. This is used
194         to see if there is any progress when non-fatal errors have occured
195         (e.g. tracker timeout).
196         @return An integer.
197         """
198         if self.stats is None:
199             return 0
200
201         # Determine if we need statsobj to be requested, same as for spew
202         statsobj = self.stats['stats']
203         return statsobj.numSeeds+statsobj.numPeers
204         
205     def get_num_seeds_peers(self):
206         """
207         Returns the sum of the number of seeds and peers. This function
208         works only if the Download.set_state_callback() / 
209         Session.set_download_states_callback() was called with the getpeerlist 
210         parameter set to True, otherwise returns (None,None)  
211         @return A tuple (num seeds, num peers)
212         """
213         if self.stats is None or self.stats['spew'] is None:
214             return (None,None)
215         
216         total = len(self.stats['spew'])
217         seeds = len([i for i in self.stats['spew'] if i['completed'] == 1.0])
218         return seeds, total-seeds
219     
220     def get_pieces_complete(self):
221         """ Returns a list of booleans indicating whether we have completely
222         received that piece of the content. The list of pieces for which 
223         we provide this info depends on which files were selected for download
224         using DownloadStartupConfig.set_selected_files().
225         @return A list of booleans
226         """
227         if self.stats is None:
228             return []
229         else:
230             return self.haveslice
231
232     def get_vod_prebuffering_progress(self):
233         """ Returns the percentage of prebuffering for Video-On-Demand already 
234         completed.
235         @return A float (0..1) """
236         if self.stats is None:
237             if self.status == DLSTATUS_STOPPED and self.progress == 1.0:
238                 return 1.0
239             else:
240                 return 0.0
241         else:
242             return self.stats['vod_prebuf_frac']
243     
244     def is_vod(self):
245         """ Returns if this download is currently in vod mode 
246         
247         @return A Boolean"""
248         if self.stats is None:
249             return False
250         else:
251             return self.stats['vod']
252     
253     def get_vod_playable(self):
254         """ Returns whether or not the Download started in Video-On-Demand
255         mode has sufficient prebuffer and download speed to be played out
256         to the user. 
257         @return Boolean.
258         """
259         if self.stats is None:
260             return False
261         else:
262             return self.stats['vod_playable']
263
264     def get_vod_playable_after(self):
265         """ Returns the estimated time until the Download started in Video-On-Demand
266         mode can be started to play out to the user. 
267         @return A number of seconds.
268         """
269         if self.stats is None:
270             return float(2 ** 31)
271         else:
272             return self.stats['vod_playable_after']
273         
274     def get_vod_stats(self):
275         """ Returns a dictionary of collected VOD statistics. The keys contained are:
276         <pre>
277         'played' = number of pieces played. With seeking this may be more than npieces
278         'late' = number of pieces arrived after they were due
279         'dropped' = number of pieces lost
280         'stall' = estimation of time the player stalled, waiting for pieces (seconds)
281         'pos' = playback position, as an absolute piece number
282         'prebuf' = amount of prebuffering time that was needed (seconds,
283                    set when playback starts)
284         'firstpiece' = starting absolute piece number of selected file
285         'npieces' = number of pieces in selected file
286         </pre>, or no keys if no VOD is in progress.
287         @return Dict.
288         """
289         if self.stats is None:
290             return {}
291         else:
292             return self.stats['vod_stats']
293
294
295
296     def get_log_messages(self):
297         """ Returns the last 10 logged non-fatal error messages.
298         @return A list of (time,msg) tuples. Time is Python time() format. """
299         if self.logmsgs is None:
300             return []
301         else:
302             return self.logmsgs
303
304     def get_peerlist(self):
305         """ Returns a list of dictionaries, one for each connected peer
306         containing the statistics for that peer. In particular, the
307         dictionary contains the keys:
308         <pre>
309         'id' = PeerID or 'http seed'
310         'ip' = IP address as string or URL of httpseed
311         'optimistic' = True/False
312         'direction' = 'L'/'R' (outgoing/incoming)
313         'uprate' = Upload rate in KB/s
314         'uinterested' = Upload Interested: True/False
315         'uchoked' = Upload Choked: True/False
316         'downrate' = Download rate in KB/s
317         'dinterested' = Download interested: True/Flase
318         'dchoked' = Download choked: True/False
319         'snubbed' = Download snubbed: True/False
320         'utotal' = Total uploaded from peer in KB
321         'dtotal' = Total downloaded from peer in KB
322         'completed' = Fraction of download completed by peer (0-1.0) 
323         'speed' = The peer's current total download speed (estimated)
324         </pre>
325         """
326         if self.stats is None or 'spew' not in self.stats:
327             return []
328         else:
329             return self.stats['spew']
330
331
332     def get_coopdl_helpers(self):
333         """ Returns the peers currently helping.
334         @return A list of PermIDs.
335         """
336         if self.coopdl_helpers is None:
337             return []
338         else:
339             return self.coopdl_helpers 
340
341     def get_coopdl_coordinator(self):
342         """ Returns the permid of the coordinator when helping that peer
343         in a cooperative download
344         @return A PermID.
345         """
346         return self.coopdl_coordinator
347
348     #
349     # RePEX: get swarmcache
350     #
351     def get_swarmcache(self):
352         """
353         Gets the SwarmCache of the Download. If the Download was RePEXing,
354         the latest SwarmCache is returned. If the Download was running 
355         normally, a sample of the peerlist is merged with the last
356         known SwarmCache. If the Download was stopped, the last known
357         SwarmCache is returned.
358         
359         @return The latest SwarmCache for this Download, which is a dict 
360         mapping dns to a dict with at least 'last_seen' and 'pex' keys.
361         """
362         swarmcache = {}
363         if self.status == DLSTATUS_REPEXING and self.swarmcache is not None:
364             # the swarmcache given at construction comes from RePEXer 
365             swarmcache = self.swarmcache
366         elif self.status in [DLSTATUS_DOWNLOADING, DLSTATUS_SEEDING]:
367             # get local PEX peers from peerlist and fill swarmcache
368             peerlist = [p for p in self.get_peerlist() if p['direction']=='L' and p.get('pex_received',0)][:REPEX_SWARMCACHE_SIZE]
369             swarmcache = {}
370             for peer in peerlist:
371                 dns = (peer['ip'], peer['port'])
372                 swarmcache[dns] = {'last_seen':self.time,'pex':[]}
373             # fill remainder with peers from old swarmcache
374             if self.swarmcache is not None:
375                 for dns in self.swarmcache.keys()[:REPEX_SWARMCACHE_SIZE-len(swarmcache)]:
376                     swarmcache[dns] = self.swarmcache[dns]
377             
378             # TODO: move peerlist sampling to a different module?
379             # TODO: perform swarmcache computation only once?
380         elif self.swarmcache is not None:
381             # In all other cases, use the old swarmcache
382             swarmcache = self.swarmcache
383             # TODO: rearrange if statement to merge 1st and 3rd case?
384             
385         return swarmcache
386