1 # Written by Arno Bakker, Choopan RATTANAPOKA, Jie Yang
2 # see LICENSE.txt for license information
3 """ Base class for Player and Plugin Background process. See swarmplayer.py """
6 # TODO: set 'download_slice_size' to 32K, such that pieces are no longer
7 # downloaded in 2 chunks. This particularly avoids a bad case where you
8 # kick the source: you download chunk 1 of piece X
9 # from lagging peer and download chunk 2 of piece X from source. With the piece
10 # now complete you check the sig. As the first part of the piece is old, this
11 # fails and we kick the peer that gave us the completing chunk, which is the
14 # Note that the BT spec says:
15 # "All current implementations use 2 15 , and close connections which request
16 # an amount greater than 2 17." http://www.bittorrent.org/beps/bep_0003.html
18 # So it should be 32KB already. However, the BitTorrent (3.4.1, 5.0.9),
19 # BitTornado and Azureus all use 2 ** 14 = 16KB chunks.
27 from base64 import encodestring
28 from threading import enumerate,currentThread,RLock
29 from traceback import print_exc
31 from BaseLib.Video.utils import svcextdefaults
33 if sys.platform == "darwin":
34 # on Mac, we can only load VLC/OpenSSL libraries
35 # relative to the location of tribler.py
36 os.chdir(os.path.abspath(os.path.dirname(sys.argv[0])))
39 wxversion.select('2.8')
44 from BaseLib.__init__ import LIBRARYNAME
45 from BaseLib.Core.API import *
46 from BaseLib.Policies.RateManager import UserDefinedMaxAlwaysOtherwiseEquallyDividedRateManager
47 from BaseLib.Utilities.Instance2Instance import *
49 from BaseLib.Player.systray import *
50 # from BaseLib.Player.Reporter import Reporter
51 from BaseLib.Player.UtilityStub import UtilityStub
52 from BaseLib.Core.Statistics.Status.Status import get_status_holder
57 DISKSPACE_LIMIT = 5L * 1024L * 1024L * 1024L # 5 GB
58 DEFAULT_MAX_UPLOAD_SEED_WHEN_SEEDING = 75 # KB/s
60 class BaseApp(wx.App,InstanceConnectionHandler):
61 def __init__(self, redirectstderrout, appname, appversion, params, single_instance_checker, installdir, i2iport, sport):
62 self.appname = appname
63 self.appversion = appversion
65 self.single_instance_checker = single_instance_checker
66 self.installdir = installdir
67 self.i2iport = i2iport
73 self.downloads_in_vodmode = Set() # Set of playing Downloads, one for SP, many for Plugin
74 self.ratelimiter = None
75 self.ratelimit_update_count = 0
76 self.playermode = DLSTATUS_DOWNLOADING
77 self.getpeerlistcount = 2 # for research Reporter
78 self.shuttingdown = False
80 InstanceConnectionHandler.__init__(self,self.i2ithread_readlinecallback)
81 wx.App.__init__(self, redirectstderrout)
85 """ To be wrapped in a OnInit() method that returns True/False """
89 state_dir = Session.get_default_state_dir('.'+self.appname)
91 self.utility = UtilityStub(self.installdir,state_dir)
92 self.utility.app = self
93 print >>sys.stderr,self.utility.lang.get('build')
94 self.iconpath = os.path.join(self.installdir,LIBRARYNAME,'Images',self.appname+'Icon.ico')
95 self.logopath = os.path.join(self.installdir,LIBRARYNAME,'Images',self.appname+'Logo.png')
98 # Start server for instance2instance communication
99 self.i2is = Instance2InstanceServer(self.i2iport,self,timeout=(24.0*3600.0))
102 # The playerconfig contains all config parameters that are not
103 # saved by checkpointing the Session or its Downloads.
104 self.load_playerconfig(state_dir)
106 # Install systray icon
107 # Note: setting this makes the program not exit when the videoFrame
109 self.tbicon = PlayerTaskBarIcon(self,self.iconpath)
111 # Start Tribler Session
112 cfgfilename = Session.get_default_config_filename(state_dir)
115 print >>sys.stderr,"main: Session config",cfgfilename
117 self.sconfig = SessionStartupConfig.load(cfgfilename)
119 print >>sys.stderr,"main: Session saved port",self.sconfig.get_listen_port(),cfgfilename
122 self.sconfig = SessionStartupConfig()
123 self.sconfig.set_install_dir(self.installdir)
124 self.sconfig.set_state_dir(state_dir)
125 self.sconfig.set_listen_port(self.sport)
126 self.configure_session()
128 self.s = Session(self.sconfig)
129 self.s.set_download_states_callback(self.sesscb_states_callback)
131 # self.reporter = Reporter( self.sconfig )
134 self.ratelimiter = UserDefinedMaxAlwaysOtherwiseEquallyDividedRateManager()
135 self.ratelimiter.set_global_max_speed(DOWNLOAD,DOWNLOADSPEED)
136 self.ratelimiter.set_global_max_speed(UPLOAD,90)
139 # Arno: For extra robustness, ignore any errors related to restarting
141 # Load all other downloads in cache, but in STOPPED state
142 self.s.load_checkpoint(initialdlstatus=DLSTATUS_STOPPED)
146 # Start remote control
149 # report client version
150 # from BaseLib.Core.Statistics.StatusReporter import get_reporter_instance
151 reporter = get_status_holder("LivingLab")
152 reporter.create_and_add_event("client-startup-version", [self.utility.lang.get("version")])
153 reporter.create_and_add_event("client-startup-build", [self.utility.lang.get("build")])
154 reporter.create_and_add_event("client-startup-build-date", [self.utility.lang.get("build_date")])
156 def configure_session(self):
158 self.sconfig.set_overlay(False)
159 self.sconfig.set_megacache(False)
162 def _get_poa(self, tdef):
163 """Try to load a POA - possibly trigger a GUI-thing or should the plugin handle getting it if none is already available?"""
165 from BaseLib.Core.ClosedSwarm import ClosedSwarm,PaymentIntegration
166 print >>sys.stderr, "Swarm_id:",encodestring(tdef.infohash).replace("\n","")
168 poa = ClosedSwarm.trivial_get_poa(self.s.get_state_dir(),
173 if not poa.torrent_id == tdef.infohash:
174 raise Exception("Bad POA - wrong infohash")
175 print >> sys.stderr,"Loaded poa from ",self.s.get_state_dir()
177 # Try to get it or just let the plugin handle it?
178 swarm_id = encodestring(tdef.infohash).replace("\n","")
179 my_id = encodestring(self.s.get_permid()).replace("\n", "")
181 # TODO: Support URLs from torrents?
182 poa = PaymentIntegration.wx_get_poa(None,
185 swarm_title=tdef.get_name())
187 print >> sys.stderr, "Failed to get POA:",e
191 ClosedSwarm.trivial_save_poa(self.s.get_state_dir(),
196 print >> sys.stderr,"Failed to save POA",e
199 if not poa.torrent_id == tdef.infohash:
200 raise Exception("Bad POA - wrong infohash")
205 def start_download(self,tdef,dlfile,poa=None,supportedvodevents=None):
206 """ Start download of torrent tdef and play video file dlfile from it """
208 from BaseLib.Core.ClosedSwarm import ClosedSwarm
209 if not poa.__class__ == ClosedSwarm.POA:
210 raise InvalidPOAException("Not a POA")
212 # Free diskspace, if needed
213 destdir = self.get_default_destdir()
214 if not os.access(destdir,os.F_OK):
217 # Arno: For extra robustness, ignore any errors related to restarting
218 # TODO: Extend code such that we can also delete files from the
219 # disk cache, not just Downloads. This would allow us to keep the
220 # parts of a Download that we already have, but that is being aborted
221 # by the user by closing the video window. See remove_playing_*
223 if not self.free_up_diskspace_by_downloads(tdef.get_infohash(),tdef.get_length([dlfile])):
224 print >>sys.stderr,"main: Not enough free diskspace, ignoring"
228 # Setup how to download
229 dcfg = DownloadStartupConfig()
234 print >> sys.stderr,"POA:",dcfg.get_poa()
238 # Delegate processing to VideoPlayer
239 if supportedvodevents is None:
240 supportedvodevents = self.get_supported_vod_events()
242 print >>sys.stderr,"bg: VOD EVENTS",supportedvodevents
243 dcfg.set_video_events(supportedvodevents)
246 if tdef.is_multifile_torrent():
247 svcdlfiles = self.is_svc(dlfile, tdef)
249 if svcdlfiles is not None:
250 dcfg.set_video_event_callback(self.sesscb_vod_event_callback, dlmode=DLMODE_SVC)
251 # Ric: svcdlfiles is an ordered list of svc layers
252 dcfg.set_selected_files(svcdlfiles)
254 # Normal multi-file torrent
255 dcfg.set_video_event_callback(self.sesscb_vod_event_callback)
256 dcfg.set_selected_files([dlfile])
258 dcfg.set_video_event_callback(self.sesscb_vod_event_callback)
259 # Do not set selected file
262 dcfg.set_dest_dir(destdir)
264 # Arno: 2008-7-15: commented out, just stick with old ABC-tuned
266 #dcfg.set_max_conns_to_initiate(300)
267 #dcfg.set_max_conns(300)
270 print >>sys.stderr,"bg: Capping Download speed to 1 MByte/s"
271 dcfg.set_max_speed(DOWNLOAD,1024)
274 # Stop all non-playing, see if we're restarting one
275 infohash = tdef.get_infohash()
277 for d in self.s.get_downloads():
278 if d.get_def().get_infohash() == infohash:
279 # Download already exists.
280 # One safe option is to remove it (but not its downloaded content)
281 # so we can start with a fresh DownloadStartupConfig. However,
282 # this gives funky concurrency errors and could prevent a
283 # Download from starting without hashchecking (as its checkpoint
285 # Alternative is to set VOD callback, etc. at Runtime:
286 print >>sys.stderr,"main: Reusing old duplicate Download",`infohash`
289 # If we have a POA, we add it to the existing download
293 if d not in self.downloads_in_vodmode:
296 self.s.lm.h4xor_reset_init_conn_counter()
298 # ARNOTODO: does this work with Plugin's duplicate download facility?
300 self.playermode = DLSTATUS_DOWNLOADING
302 print >>sys.stderr,"main: Starting new Download",`infohash`
303 newd = self.s.start_download(tdef,dcfg)
304 # Ric: added restart of an svc download
306 newd.set_video_events(self.get_supported_vod_events())
308 svcdlfiles = self.is_svc(dlfile, tdef)
309 if svcdlfiles is not None:
310 newd.set_video_event_callback(self.sesscb_vod_event_callback, dlmode = DLMODE_SVC)
311 # Ric: svcdlfiles is an ordered list of svc layers
312 newd.set_selected_files(svcdlfiles)
314 newd.set_video_event_callback(self.sesscb_vod_event_callback)
315 if tdef.is_multifile_torrent():
316 newd.set_selected_files([dlfile])
318 print >>sys.stderr,"main: Restarting existing Download",`infohash`
321 self.downloads_in_vodmode.add(newd)
323 print >>sys.stderr,"main: Saving content to",newd.get_dest_files()
327 def sesscb_vod_event_callback(self,d,event,params):
330 def get_supported_vod_events(self):
337 def free_up_diskspace_by_downloads(self,infohash,needed):
340 print >> sys.stderr,"main: free_up: needed",needed,DISKSPACE_LIMIT
341 if needed > DISKSPACE_LIMIT:
342 # Not cleaning out whole cache for bigguns
344 print >> sys.stderr,"main: free_up: No cleanup for bigguns"
349 dlist = self.s.get_downloads()
351 hisinfohash = d.get_def().get_infohash()
352 if infohash == hisinfohash:
353 # Don't delete the torrent we want to play
355 destfiles = d.get_dest_files()
357 print >> sys.stderr,"main: free_up: Downloaded content",`destfiles`
360 for (filename,savepath) in destfiles:
361 stat = os.stat(savepath)
362 dinuse += stat.st_size
364 timerec = (stat.st_ctime,dinuse,d)
365 timelist.append(timerec)
367 if inuse+needed < DISKSPACE_LIMIT:
368 # Enough available, done.
370 print >> sys.stderr,"main: free_up: Enough avail",inuse
373 # Policy: remove oldest till sufficient
376 print >> sys.stderr,"main: free_up: Found",timelist,"in dest dir"
379 for ctime,dinuse,d in timelist:
380 print >> sys.stderr,"main: free_up: Removing",`d.get_def().get_name_as_unicode()`,"to free up diskspace, t",ctime
381 self.s.remove_download(d,removecontent=True)
385 # Deleted all, still no space:
390 # Process periodically reported DownloadStates
392 def sesscb_states_callback(self,dslist):
393 """ Called by Session thread """
395 #print >>sys.stderr,"bg: sesscb_states_callback",currentThread().getName()
398 if (int(time.time()) % 5) == 0:
400 d = ds.get_download()
401 print >>sys.stderr, '%s %s %5.2f%% %s up %8.2fKB/s down %8.2fKB/s' % \
402 (d.get_def().get_name(), \
403 dlstatus_strings[ds.get_status()], \
404 ds.get_progress() * 100, \
406 ds.get_current_speed(UPLOAD), \
407 ds.get_current_speed(DOWNLOAD))
409 # Arno: we want the prebuf stats every second, and we want the
410 # detailed peerlist, needed for research stats. Getting them every
411 # second may be too expensive, so get them every 10.
413 self.getpeerlistcount += 1
414 getpeerlist = (self.getpeerlistcount % 10) == 0
415 haspeerlist = (self.getpeerlistcount % 10) == 1
417 # Arno: delegate to GUI thread. This makes some things (especially
418 #access control to self.videoFrame easier
419 #self.gui_states_callback(dslist)
420 #print >>sys.stderr,"bg: sesscb_states_callback: calling GUI",currentThread().getName()
421 wx.CallAfter(self.gui_states_callback_wrapper,dslist,haspeerlist)
423 #print >>sys.stderr,"main: SessStats:",self.getpeerlistcount,getpeerlist,haspeerlist
424 return (1.0,getpeerlist)
427 def gui_states_callback_wrapper(self,dslist,haspeerlist):
429 self.gui_states_callback(dslist,haspeerlist)
434 def gui_states_callback(self,dslist,haspeerlist):
435 """ Called by *GUI* thread.
436 CAUTION: As this method is called by the GUI thread don't to any
437 time-consuming stuff here! """
439 #print >>sys.stderr,"main: Stats:"
440 if self.shuttingdown:
443 # See which Download is currently playing
444 playermode = self.playermode
447 totalspeed[UPLOAD] = 0.0
448 totalspeed[DOWNLOAD] = 0.0
451 # When not playing, display stats for all Downloads and apply rate control.
452 if playermode == DLSTATUS_SEEDING:
455 print >>sys.stderr,"main: Stats: Seeding: %s %.1f%% %s" % (dlstatus_strings[ds.get_status()],100.0*ds.get_progress(),ds.get_error())
456 self.ratelimit_callback(dslist)
458 # Calc total dl/ul speed and find DownloadStates for playing Downloads
461 if ds.get_download() in self.downloads_in_vodmode:
462 playing_dslist.append(ds)
463 elif DEBUG and playermode == DLSTATUS_DOWNLOADING:
464 print >>sys.stderr,"main: Stats: Waiting: %s %.1f%% %s" % (dlstatus_strings[ds.get_status()],100.0*ds.get_progress(),ds.get_error())
466 for dir in [UPLOAD,DOWNLOAD]:
467 totalspeed[dir] += ds.get_current_speed(dir)
468 totalhelping += ds.get_num_peers()
470 # Report statistics on all downloads to research server, every 10 secs
474 # self.reporter.report_stat(ds)
478 # Set systray icon tooltip. This has limited size on Win32!
479 txt = self.appname+' '+self.appversion+'\n\n'
480 txt += 'DL: %.1f\n' % (totalspeed[DOWNLOAD])
481 txt += 'UL: %.1f\n' % (totalspeed[UPLOAD])
482 txt += 'Helping: %d\n' % (totalhelping)
483 #print >>sys.stderr,"main: ToolTip summary",txt
484 self.OnSetSysTrayTooltip(txt)
486 # No playing Downloads
487 if len(playing_dslist) == 0:
489 elif DEBUG and playermode == DLSTATUS_DOWNLOADING:
490 for ds in playing_dslist:
491 print >>sys.stderr,"main: Stats: DL: %s %.1f%% %s dl %.1f ul %.1f n %d" % (dlstatus_strings[ds.get_status()],100.0*ds.get_progress(),ds.get_error(),ds.get_current_speed(DOWNLOAD),ds.get_current_speed(UPLOAD),ds.get_num_peers())
493 # If we're done playing we can now restart any previous downloads to
495 if playermode != DLSTATUS_SEEDING:
496 playing_seeding_count = 0
497 for ds in playing_dslist:
498 if ds.get_status() == DLSTATUS_SEEDING:
499 playing_seeding_count += 1
500 if len(playing_dslist) == playing_seeding_count:
501 self.restart_other_downloads()
503 # cf. 25 Mbps cap to reduce CPU usage and improve playback on slow machines
504 # Arno: on some torrents this causes VLC to fail to tune into the video
505 # although it plays audio???
506 #ds.get_download().set_max_speed(DOWNLOAD,1500)
509 return (playing_dslist,totalhelping,totalspeed)
512 def OnSetSysTrayTooltip(self,txt):
513 if self.tbicon is not None:
514 self.tbicon.set_icon_tooltip(txt)
517 # Download Management
519 def restart_other_downloads(self):
520 """ Called by GUI thread """
521 if self.shuttingdown:
523 print >>sys.stderr,"main: Restarting other downloads"
524 self.playermode = DLSTATUS_SEEDING
525 self.ratelimiter = UserDefinedMaxAlwaysOtherwiseEquallyDividedRateManager()
526 self.set_ratelimits()
528 dlist = self.s.get_downloads()
530 if d not in self.downloads_in_vodmode:
531 d.set_mode(DLMODE_NORMAL) # checkpointed torrents always restarted in DLMODE_NORMAL, just make extra sure
535 def remove_downloads_in_vodmode_if_not_complete(self):
536 print >>sys.stderr,"main: Removing playing download if not complete"
537 for d in self.downloads_in_vodmode:
538 d.set_state_callback(self.sesscb_remove_playing_callback)
540 def sesscb_remove_playing_callback(self,ds):
541 """ Called by SessionThread """
543 print >>sys.stderr,"main: sesscb_remove_playing_callback: status is",dlstatus_strings[ds.get_status()],"progress",ds.get_progress()
545 d = ds.get_download()
546 name = d.get_def().get_name()
547 if (ds.get_status() == DLSTATUS_DOWNLOADING and ds.get_progress() >= 0.9) or ds.get_status() == DLSTATUS_SEEDING:
549 print >>sys.stderr,"main: sesscb_remove_playing_callback: voting for KEEPING",`name`
551 print >>sys.stderr,"main: sesscb_remove_playing_callback: voting for REMOVING",`name`
552 if self.shuttingdown:
553 # Arno, 2010-04-23: Do it now ourselves, wx won't do it anymore. Saves
554 # hashchecking on sparse file on Linux.
555 self.remove_playing_download(d)
557 wx.CallAfter(self.remove_playing_download,d)
562 def remove_playing_download(self,d):
563 """ Called by MainThread """
564 if self.s is not None:
565 print >>sys.stderr,"main: Removing incomplete download",`d.get_def().get_name_as_unicode()`
567 self.s.remove_download(d,removecontent=True)
568 self.downloads_in_vodmode.remove(d)
572 def stop_playing_download(self,d):
573 """ Called by MainThread """
574 print >>sys.stderr,"main: Stopping download",`d.get_def().get_name_as_unicode()`
577 self.downloads_in_vodmode.remove(d)
585 def set_ratelimits(self):
586 uploadrate = float(self.playerconfig['total_max_upload_rate'])
587 print >>sys.stderr,"main: set_ratelimits: Setting max upload rate to",uploadrate
588 if self.ratelimiter is not None:
589 self.ratelimiter.set_global_max_speed(UPLOAD,uploadrate)
590 self.ratelimiter.set_global_max_seedupload_speed(uploadrate)
592 def ratelimit_callback(self,dslist):
593 """ When the player is in seeding mode, limit the used upload to
594 the limit set by the user via the options menu.
595 Called by *GUI* thread """
596 if self.ratelimiter is None:
599 # Adjust speeds once every 4 seconds
601 if self.ratelimit_update_count % 4 == 0:
603 self.ratelimit_update_count += 1
606 self.ratelimiter.add_downloadstatelist(dslist)
607 self.ratelimiter.adjust_speeds()
613 def load_playerconfig(self,state_dir):
614 self.playercfgfilename = os.path.join(state_dir,'playerconf.pickle')
615 self.playerconfig = None
617 f = open(self.playercfgfilename,"rb")
618 self.playerconfig = pickle.load(f)
622 self.playerconfig = {}
623 self.playerconfig['total_max_upload_rate'] = DEFAULT_MAX_UPLOAD_SEED_WHEN_SEEDING # KB/s
625 def save_playerconfig(self):
627 f = open(self.playercfgfilename,"wb")
628 pickle.dump(self.playerconfig,f)
633 def set_playerconfig(self,key,value):
634 self.playerconfig[key] = value
636 if key == 'total_max_upload_rate':
638 self.set_ratelimits()
642 def get_playerconfig(self,key):
643 return self.playerconfig[key]
650 print >>sys.stderr,"main: ONEXIT",currentThread().getName()
651 self.shuttingdown = True
652 self.remove_downloads_in_vodmode_if_not_complete()
654 # To let Threads in Session finish their business before we shut it down.
657 if self.s is not None:
658 self.s.shutdown(hacksessconfcheckpoint=False)
660 if self.tbicon is not None:
661 self.tbicon.RemoveIcon()
662 self.tbicon.Destroy()
666 print >>sys.stderr,"main: ONEXIT: Thread still running",t.getName(),"daemon",t.isDaemon()
671 def clear_session_state(self):
672 """ Try to fix apps by doing hard reset. Called from systray menu """
674 if self.s is not None:
675 dlist = self.s.get_downloads()
677 self.s.remove_download(d,removecontent=True)
680 time.sleep(1) # give network thread time to do stuff
682 dldestdir = self.get_default_destdir()
683 shutil.rmtree(dldestdir,True) # ignore errors
687 dlcheckpointsdir = os.path.join(self.s.get_state_dir(),STATEDIR_DLPSTATE_DIR)
688 shutil.rmtree(dlcheckpointsdir,True) # ignore errors
692 cfgfilename = os.path.join(self.s.get_state_dir(),STATEDIR_SESSCONFIG)
693 os.remove(cfgfilename)
697 self.s = None # HARD EXIT
699 sys.exit(0) # DIE HARD 4.0
702 def show_error(self,msg):
703 dlg = wx.MessageDialog(None, msg, self.appname+" Error", wx.OK|wx.ICON_ERROR)
704 result = dlg.ShowModal()
708 def get_default_destdir(self):
709 return os.path.join(self.s.get_state_dir(),'downloads')
712 def is_svc(self, dlfile, tdef):
713 """ Ric: check if it as an SVC download. If it is add the enhancement
714 layers to the dlfiles
718 if tdef.is_multifile_torrent():
719 enhancement = tdef.get_files(exts=svcextdefaults)
720 # Ric: order the enhancement layer in the svcfiles list
721 # if the list of enhancements is not empty
724 if tdef.get_length(enhancement[0]) == tdef.get_length(dlfile):
726 svcfiles.extend(enhancement)
731 # InstanceConnectionHandler
733 def i2ithread_readlinecallback(self,ic,cmd):