#! /usr/bin/env python
# -*- coding: utf-8 -*-
#-----------------------------------------------------------------------------
# Name: btsession.py
# Purpose:
#
# Author: Jeremy Arendt
#
# Created: 2004/02/02
# RCS-ID: $Id: btsession.py,v 1.8 2005/10/25 06:55:23 inigo Exp $
# Copyright: (c) 2002
# Licence: See G3.LICENCE.TXT
#-----------------------------------------------------------------------------
from sys import argv
from os.path import dirname,join,split,exists,abspath,isdir
from urlparse import urlparse
from os import getcwd,mkdir
from shutil import copyfile,move
from BitTorrent import version
from BitTorrent.bencode import bencode,bdecode
from BitTorrent.download import Download
from BitTorrent.zurllib import quote
from threading import Event,Thread,enumerate
import wx
import sys
from time import time
from traceback import print_exc
from btconfig import BTConfig
from time import time
from random import shuffle
from types import UnicodeType,StringType
from BitTorrent.encodedata import encodefile
import os
import os.path
import locale
import encodings
try:
import cjkcodecs.aliases
except:
pass
try:
import iconv_codec
except:
pass
if sys.platform == "win32":
win32_flag = True
else:
win32_flag = False
if win32_flag:
from os import startfile
from webbrowser import open_new
else:
from leoxv import startfile,open_new
class BTSession:
def __init__(self, parent, session_id, peer_id, invokefunc, updatefunc,
errorfunc, onstartfunc, friendfunc, btconfig, images, params = None,
confirm_dlpath=False, addmsgfunc=None, orderMngrfunc=None, path="./"):
self.path = path
self.images = images
self.parent = parent
self.doneflag = Event()
self.setupflag = Event()
self.params = []
self.config = {}
self.FriendFunc = friendfunc
self.AddMsg = addmsgfunc
self.AlertParent = errorfunc
self.UpdateParent = updatefunc
self.InvokeLater = invokefunc
self.DLOrderMngrDlg = orderMngrfunc
self.onstartfunc = onstartfunc
self.btconfig = btconfig
self.record_len = 601
self.record_urate = [0] * self.record_len
self.record_drate = [0] * self.record_len
self.spewflag = Event()
self.spewflag.set()
self.filename = 'Examining file...'
self.filesize = 0
self.responsefile = None
self.confirm_dlpath = confirm_dlpath
self.complete = False # file download complete, thread is still sharing
self.started = False # dl thread has started at least once
self.failed = False # session is a failure
self.done = False # session was terminated
self.stopped = False #stopped mode
self.paused = True # session is paused
self.checking = False # dl thread in checking/startup phase
self.restarting = False # The session is in the process of a restart
self.fintime = 0 # timestamp when the DL completed
self.queue_rank = -9
self.status_data = None
self.static_data = None
self.error_count = {'tracker_refuse':0, 'tracker_timeout':0, 'tracker_404':0}
self.id = session_id
self.peer_id = peer_id
self.download = Download()
self.last_update_time = 0
self.last_parent_update = None
self.responsefile_moved = False
self.file_moved = False
self.file_move_in_progress = False
self.scrape_data = None
self.tracker_url = None
self.spew = False
self.Setup_Thread(params) # used to be a thread... not anymore
self.thread = Thread(name="DLThread %d"%session_id, target = self.Download_Thread, args = [])
self.thread.setDaemon(False)
if self.btconfig.Get('torrent_state') == 0:
pass
elif self.btconfig.Get('torrent_state') == 1:
self.Pause()
elif self.btconfig.Get('torrent_state') == 2:
self.Stop()
def SetPeerId(self, id):
self.peer_id = id
def ToggleSpew(self, value):
self.download.ToggleSpew(value)
# The session has to be restarted after this is set
def ChangePortRange(self, minport, maxport):
self.download.config['minport'] = minport
self.download.config['maxport'] = maxport
self.Restart()
def AppendRateRecord(self, drate, urate):
if drate == None:
drate = 0
if urate == None:
urate = 0
self.record_drate.pop(0)
self.record_urate.pop(0)
self.record_drate.append(int(drate/1024))
self.record_urate.append(int(urate/1024))
def GetFinTime(self):
return self.fintime
def GetRecords(self):
return (self.record_drate, self.record_urate)
def GetQRank(self):
return self.queue_rank
def SetQRank(self, rank):
self.queue_rank = rank
def GetId(self):
return self.id
def GetScrapeData(self):
return self.scrape_data
def GetStatusData(self):
return self.status_data
def SetStatusData(self, d):
self.status_data = d
def GetStat(self, param):
if self.status_data:
return self.status_data.get(param)
else:
return None
def GetStaticData(self):
return self.static_data
def SetStaticData(self, d):
self.static_data = d
def IsPaused(self):
return self.paused
def IsManualPause(self):
return self.stopped
def IsStopped(self):
return self.stopped
def HasFailed(self):
return self.failed
def IsDone(self):
if self.done or self.failed:
return True
return False
def IsChecking(self):
return self.checking
def IsComplete(self):
return self.complete
def IsRunning(self):
if self.file_move_in_progress or (self.started and self.doneflag.isSet() == False
and hasattr(self, 'thread') and self.thread.isAlive() and self.download != None):
return True
return False
def _Cfg2Params(self, config):
params = []
try:
params.append( '--responsefile' )
params.append( config['responsefile'] )
params.append( '--saveas' )
params.append( config['saveas'] )
params.append( '--filesize' )
params.append( config['filesize'] )
params.append( '--up_total' )
params.append( config['up_total'] )
params.append( '--down_total' )
params.append( config['down_total'] )
params.append( '--check_hashes' )
params.append( config['check_hashes'] )
except KeyError:
return None
return params
def GetSaveParams(self):
params = None
if self.download and self.download.GetConfig():
params = self._Cfg2Params(self.download.GetConfig())
if self.IsRunning():
params[7] = self.download.GetUpTotal()
params[9] = self.download.GetDownTotal()
if params == None:
params = self.params[0:4]
if self.IsRunning():
state = 0
elif self.IsStopped():
state = 2
elif self.IsPaused():
state = 1
else:
state = -1
# the keys we want to save for the next time this torrent is started
cfg = {
'file_ranges' : self.btconfig.Get('file_ranges'),
'random_order' : self.btconfig.Get('random_order'),
'end_on_percent': self.btconfig.Get('end_on_percent'),
'end_on_ratio' : self.btconfig.Get('end_on_ratio'),
'end_on_timelimit' : self.btconfig.Get('end_on_timelimit'),
'end_percent' : self.btconfig.Get('end_percent'),
'end_ratio' : self.btconfig.Get('end_ratio'),
'end_timelimit' : self.btconfig.Get('end_timelimit'),
'on_complete' : self.btconfig.Get('on_complete'),
'maxuploads' : self.btconfig.Get('maxuploads'),
'maxupspeed' : self.btconfig.Get('maxupspeed'),
'max_initiate' : self.btconfig.Get('max_initiate'),
'max_connections': self.btconfig.Get('max_connections'),
'use_global_urate': self.btconfig.Get('use_global_urate'),
'choker' : self.btconfig.Get('choker'),
'choke_period' : self.btconfig.Get('choke_period'),
'opt_choke_period' : self.btconfig.Get('opt_choke_period'),
'choke_minrate' : self.btconfig.Get('choke_minrate'),
'choke_udban' : self.btconfig.Get('choke_udban'),
'choke_activate' : self.btconfig.Get('choke_activate'),
'choke_multiplier' : self.btconfig.Get('choke_multiplier'),
'torrent_state' : state,
}
params.append('--cfg')
params.append(str(cfg))
return params
def GetSavePeers(self):
peers = []
spew = self.GetStat('spew')
if spew != None:
for c in spew:
peers.append( [ c['ip'], c['port'], c['peerid'] ] )
return peers
def GetDownload(self):
return self.download
def GetCfg(self):
return self.btconfig
def GetResponseFilename(self):
return self.responsefile
def GetFilename(self):
return self.filename
def GetFilesize(self):
return self.filesize
def GetFileRanges(self):
file_ranges = self.btconfig.Get('file_ranges')
if self.IsRunning() and self.status_data and file_ranges and len(file_ranges) > 0:
havelist = self.status_data['havelist']
# get current progress
for file in file_ranges:
havecount = 0
if havelist:
totalcount = len(havelist[file[1]:file[2]])
for h in havelist[file[1]:file[2]]:
if h:
havecount += 1
if havecount > 0:
fraction = float(havecount) / totalcount
else:
fraction = 0
else:
# status data hasnt arrived yet
if self.IsComplete():
fraction = 1.0
else:
fraction = 0.0
file[3] = fraction
return file_ranges
elif file_ranges and len(file_ranges) > 0:
return file_ranges
else:
return []
def IsDLOrderRandom(self):
return self.btconfig.Get('random_order')
def Show(self):
pass
def Kill(self):
print '* Killing btsession ' + str(self.id)
self.paused = False
self.done = True
self.doneflag.set()
self.setupflag.set()
try:
if self.thread.isAlive():
self.thread.join(5)
if self.thread.isAlive():
#TODO: find a way to terminate this thread when it won't die with dignity
print 'MOMMY!!!!'
except:
print 'Error: During Kill()'
def Stop(self):
print 'stopped ' + str(self.id)
if self.IsRunning() or self.IsPaused():
self.btconfig.Set('torrent_state', 2)
self.AddMsg("General", "Stopped %s" % self.GetFilename())
self.checking = False
self.stopped = True
self.paused = True
self.doneflag.set()
if self.started:
self.config = self.download.GetConfig()
if self.last_parent_update is not None:
print 'updating parent with stopped status'
params = self.last_parent_update
params['activity'] = 'Stopped'
params['drate'] = 0
params['urate'] = 0
params['timeleft'] = -1
params['spew'] = []
self.UpdateParent(self.id, params)
def Restart(self):
if self.IsRunning():
self.restarting = True
self.Pause()
def Pause(self):
print 'paused ' + str(self.id)
if self.IsRunning() and not self.IsPaused():
self.btconfig.Set('torrent_state', 1)
self.AddMsg("General", "Paused %s" % self.GetFilename())
self.config = self.download.GetConfig()
self.doneflag.set()
self.checking = False
self.paused = True
self.stopped = False
if self.last_parent_update is not None:
params = self.last_parent_update
params['activity'] = 'Paused'
params['drate'] = 0
params['urate'] = 0
params['timeleft'] = -1
params['spew'] = []
self.UpdateParent(self.id, params)
def OpenDataFolder(self):
if self.failed:
return False
else:
saveas = self.download.config['saveas']
if isdir(saveas):
orig_dirname = saveas
else:
orig_dirname, filename = split(saveas)
try:
startfile(orig_dirname)
except UnicodeEncodeError:
self.AddMsg(_("Error"), _("ERROR Data folder name encoding not supported"), -1)
return True
return False
def Resume(self):
if self.failed:
return False
elif not self.started:
self.btconfig.Set('torrent_state', 0)
self.doneflag.clear()
self.Start()
return True
elif self.IsPaused():
self.btconfig.Set('torrent_state', 0)
self.AddMsg(_("General"), _("Resuming %s") % self.GetFilename())
print 'resuming ' + str(self.id)
self.stopped = False
self.paused = False
self.checking = True
self.doneflag.clear()
self.params = self._Cfg2Params(self.config)
self.thread = Thread(name="DL-RThread %d"%self.id, target = self.Download_Thread, args = [])
self.thread.setDaemon(True)
self.thread.start()
return True
return False
def Start(self):
if self.done:
return
self.started = True
self.stopped = False
self.paused = False
self.firststart = True
self.checking = True
self.thread.start()
def StartConnection(self, ip, port, peer_id):
if self.IsRunning():
self.download.StartConnection(ip, port, peer_id)
def ReAnnounce(self, url=None):
if self.IsRunning() and not self.checking:
self.download.ReAnnounce(url)
def ReChoke(self):
if self.IsRunning():
self.download.ReChoke()
def OnError(self, error_msg, code=None):
# print "Got Error: %s, code: %s" % (error_msg, code)
if self.AlertParent:
self.AlertParent(self.id, error_msg, code)
if not self.started:
self.failed = True
def ScrapeCatcher(self, success=False, data=None):
print self, success, data
if success and data:
self.scrape_data = data
def LoadPeers(self):
if win32_flag:
peerfile = join(self.path, 'peers.ben')
else:
peerfile = join(os.path.expanduser('~/.Rufus'), 'peers.ben')
try:
file = open(peerfile, 'rb')
filedata = file.read()
file.close()
peerdata = bdecode(filedata)
except (ValueError, IOError), msg:
print 'ERROR: while loading peer', msg
return
for filename, torrent in peerdata:
if filename == encodefile(self.filename):
for peer in torrent:
self.download.StartConnection(peer[0], peer[1], peer[2])
break
def OnStart(self, d):
from scrape import T_Scrape
self.firststart = False
self.checking = False
self.started = True
self.paused = False
self.stopped = False
self.LoadPeers()
self.btconfig.SyncOptions(self.download)
self.onstartfunc(self.id, d)
php_shim = ''
ann_data = urlparse(d['announce'])
if '.php' in ann_data[2]: #needed as some SSL trackers don't work without .php extension
php_shim = '.php'
self.tracker_url = "%s://%s/scrape%s?info_hash=%s" % (ann_data[0],ann_data[1], php_shim, quote(d.get('info_hash')))
scrape = T_Scrape(self.InvokeLater, self.ScrapeCatcher, self.tracker_url, d.get('info_hash'))
scrape.start()
if not self.IsComplete():
file_ranges = self.btconfig.Get('file_ranges')
random_order = self.btconfig.Get('random_order')
if file_ranges == None or len(file_ranges) < 1:
file_ranges = self.download.GetFileRanges()
self.btconfig.Set('file_ranges', file_ranges)
if file_ranges and len(file_ranges) > 1:
self.DLOrderMngrDlg(self.id)
else:
self.SetPieceRanges(file_ranges, random_order)
def SetPieceRanges(self, file_ranges, rand=False):
if not file_ranges or len(file_ranges) == 0:
return
self.btconfig.Set('file_ranges', file_ranges)
self.btconfig.Set('random_order', rand)
if self.IsRunning():
piece_ranges = []
for d in file_ranges:
if d[4]:
piece_ranges.append( (d[1], d[2]) )
if not rand:
self.download.SetPieceRanges(piece_ranges)
else:
if self.static_data:
#piece_ranges = [(0, self.static_data['nhashes'])]
shuffle(piece_ranges)
self.download.SetPieceRanges(piece_ranges)
def RemoveResumeData(self):
self.download.KillFastResumeData()
def SaveResumeData(self):
self.download.SaveResumeFile()
def OnEnd(self, success=True):
self.record_urate = [0] * self.record_len
self.record_drate = [0] * self.record_len
if self.restarting:
self.restarting = False
self.Resume()
if not success:
self.failed = True
else:
#move DLed file to completed dir (unless already moved)
if self.IsComplete() and self.btconfig.Get("use_download_dir") and not self.file_moved and not self.confirm_dlpath:
self.file_moved = True
saveas = self.download.config['saveas']
orig_dirname, filename = split(saveas)
orig_filename = self.GetFilename()
## if type(orig_filename) is StringType:
## try:
## orig_filename = orig_filename.decode(locale.getpreferredencoding())
## except:
## orig_filename = orig_filename.decode('latin-1', 'replace')
completed_dir = self.btconfig.Get("completed_dl_dir")
if abspath(orig_dirname) != abspath(completed_dir) and abspath(saveas) != abspath(completed_dir):
self.file_move_in_progress = True
print 'moving file:'
self.AddMsg(_("General"), _("Moving file %s to %s") % (saveas, join(completed_dir, filename)))
print ' * ' + self.responsefile
print ' * ' + join(completed_dir, filename)
try:
if not exists(completed_dir):
mkdir(completed_dir)
#TODO: matching files in the torrent later.
#FIX: This stops the folder below from getting moved
if orig_filename != filename:
filename = orig_filename
saveas = join(saveas, filename)
move(saveas, join(completed_dir, filename))
self.config['saveas'] = join(completed_dir, filename)
self.download.config['saveas'] = join(completed_dir, filename)
self.params = self._Cfg2Params(self.config)
self.Setup_Thread(self.params, self.OnMoveComplete)
except:
self.file_move_in_progress = False
print "IO ERROR: Could not move file %s" % filename
print_exc()
elif not self.IsComplete():
self.download.Shutdown()
def OnMoveComplete(self):
self.file_move_in_progress = False
self.Resume()
def OnFinished(self):
self.AddMsg(_("General"), _("%s Completed") % self.GetFilename())
print 'got finished'
self.fintime = time()
self.complete = True
# notify gui that the DL finished
if self.status_data != None:
params = self.status_data
params['activity'] = 'Complete!'
params['fractionDone'] = 1.0
params['drate'] = 0
params['timeleft'] = -1
self.UpdateParent(self.id, params)
#move metafile to completed torrent dir (unless already moved)
if self.btconfig.Get("use_torrent_dir") and self.responsefile and not self.responsefile_moved and not self.confirm_dlpath:
self.responsefile_moved = True
orig_dirname, filename = split(self.responsefile)
completed_dir = self.btconfig.Get("completed_tor_dir")
if abspath(orig_dirname) != abspath(completed_dir):
print 'moving responsefile:'
self.AddMsg(_("General"), _("Moving file %s to %s") % (self.responsefile, join(completed_dir, filename)))
print ' * ' + self.responsefile
print ' * ' + join(completed_dir, filename)
try:
if not exists(completed_dir):
mkdir(completed_dir)
move(self.responsefile, join(completed_dir, filename))
except:
print "IO ERROR: Could not move file %s" %filename
print_exc()
self.params[ self.params.index("--responsefile") + 1 ] = join(completed_dir, filename)
self.download.config['responsefile'] = join(completed_dir, filename)
# prepare to move DLed file to completed dir (unless already moved)
if self.btconfig.Get("use_download_dir") and not self.file_moved and not self.confirm_dlpath:
saveas = self.download.config['saveas']
orig_dirname, filename = split(saveas)
completed_dir = self.btconfig.Get("completed_dl_dir")
if abspath(orig_dirname) != abspath(completed_dir):
self.Stop()
#set this flag so hashes will never be checked again
self.download.config['check_hashes'] = False
def OnUpdateStatus(self, d):
if self.last_update_time + 0.2 < time():
#update parent
if not self.IsPaused():
if d.get('fractionDone') == None:
fractionDone = 1
else:
fractionDone = d['fractionDone']
params = {
'filename' : self.filename,
'filesize' : self.filesize,
'fractionDone' : fractionDone,
'dtotal' : d.get('downTotal'),
'utotal' : d.get('upTotal'),
'drate' : d.get('downRate'),
'urate' : d.get('upRate'),
'timeleft' : d.get('timeEst'),
'spew' : d.get('spew'),
'havelist' : d.get('havelist'),
'availlist' : d.get('availlist'),
'peers' : d.get('npeers', 0),
'seeds' : d.get('nseeds', 0),
'dist_copies' : d.get('dist_copies'),
'avg_progress' : d.get('avg_progress')
}
self.last_parent_update = params
self.UpdateParent(self.id, params)
self.last_update_time = time()
def OnChooseFile(self, default, bucket, f, size, dir):
if type(default) is StringType:
try:
default = default.decode(locale.getpreferredencoding())
except:
try:
default = default.decode('latin-1')
except:
try:
default = default.decode('utf-8')
except:
try:
default = default.decode('iso8859_15')
except:
try:
default = default.decode('iso2022_jp_2')
except:
try:
default = default.decode('cp1251')
except:
try:
default = default.decode('iso8859_15', 'replace')
except:
try:
default = default.decode('latin-1', 'replace')
except:
pass
if dir:
dl = wx.DirDialog(self.parent, _("Choose a directory to save to, pick a partial download to resume"),
join(getcwd(), default), style = wx.DD_DEFAULT_STYLE | wx.DD_NEW_DIR_BUTTON)
else:
dl = wx.FileDialog(self.parent, _("Choose file to save as, pick a partial download to resume"), '', default, '*.*', wx.SAVE | wx.CHANGE_DIR )
if dl.ShowModal() == wx.ID_OK:
bucket[0] = dl.GetPath()
dl.Destroy()
f.set()
def ChooseFile(self, default, size, saveas, dir):
if saveas:
return saveas
if type(default) is StringType:
try:
default = default.decode('utf-8')
except:
default = default.decode('latin-1', 'replace')
if self.btconfig.Get("use_download_dir") and not self.confirm_dlpath:
dl_dir = self.btconfig.Get("download_dir")
if not exists(dl_dir):
mkdir(dl_dir)
s = join(dl_dir, default)
return s
f = Event()
bucket = [None]
#self.InvokeLater(self.OnChooseFile, [default, bucket, f, size, dir])
self.OnChooseFile(default, bucket, f, size, dir)
f.wait()
return bucket[0]
def OnNewPath(self, path):
pass
def Setup_Thread(self, params, callback=None):
print 'Starting setup thread'
if params == None or len(params) <= 1:
if params and len(params) == 1:
exists(params[0])
responsefile = params[0]
else:
b = wx.FileDialog(self.parent, _("Add From .torrent. Select file"), '', '', '*.torrent', wx.OPEN)
if b.ShowModal() != wx.ID_OK:
self.failed = True
return False
else:
responsefile = b.GetPath()
b.Destroy()
self.responsefile = responsefile
self.params.append("--responsefile")
self.params.append(responsefile)
else:
self.params = params
try:
responsefile = params [ params.index("--responsefile") + 1 ]
except ValueError:
return False
if not exists(responsefile):
self.OnEnd(False)
return False
self.params.append("--maxport")
self.params.append(self.btconfig.Get('maxport'))
self.params.append("--minport")
self.params.append(self.btconfig.Get('minport'))
if self.btconfig.Get('bind_address'):
# If the bind address is fucked up, the download thread will hang
self.params.append("--bind")
self.params.append(self.btconfig.Get('bind_address'))
if self.btconfig.Get('ip_2report'):
self.params.append("--ip")
self.params.append(self.btconfig.Get('ip_2report'))
self.params.append("--pre_allocate")
self.params.append(self.btconfig.Get('pre_allocate'))
dirname, filename = split(responsefile)
if self.btconfig.Get("use_torrent_dir") and \
dirname != self.btconfig.Get("completed_tor_dir"):
self.responsefile = join(self.btconfig.Get("torrent_dir"), filename)
self.params[ self.params.index("--responsefile") + 1 ] = self.responsefile
if dirname != self.btconfig.Get("torrent_dir"):
self.AddMsg(_("General"), _("Copying file %s to %s") % (responsefile, self.responsefile))
print 'copying file:'
print ' * ' + responsefile
print ' * ' + self.responsefile
try:
if not exists(self.btconfig.Get("torrent_dir")):
mkdir(self.btconfig.Get("torrent_dir"))
copyfile(responsefile, self.responsefile)
except:
print "IO ERROR: Could not do file operation %s" %filename
else:
self.responsefile = responsefile
resumepath = None
if self.btconfig.Get("use_resume_dir"):
resumepath = self.btconfig.Get("resume_data_dir")
try:
print 'starting dl setup'
status = self.download.setup(self.params, self.ChooseFile, self.OnUpdateStatus, self.OnFinished, self.OnError,
self.doneflag, 100, self.OnNewPath, callback=callback, resumepath=resumepath)
print 'ending dl setup'
except:
status = False
print_exc()
if status == False:
self.AddMsg(_("Error"), _("Error during setup"), -2)
print 'setup thread failed'
self.failed = True
self.doneflag.set()
self.setupflag.set()
else:
ent_type = self.download.ent_type
enc_type = self.download.enc_type
self.filename = self.download.info['name'+ent_type].decode(enc_type)
self.filesize = self.download.file_length
self.setupflag.set()
self.file_move_in_progress = False
return True
def Download_Thread(self):
print 'Entered download thread'
#don't try DL unless wait flag is set
self.setupflag.wait()
if self.doneflag.isSet():
self.AddMsg(_("Error"), _("Download thread exited on setup"), -2)
print 'download thread ending before starting'
return False
print 'Starting download thread', self.id
try:
self.download.download(self.params, self.OnUpdateStatus, self.OnFinished, self.OnError, self.FriendFunc,
self.doneflag, 100, self.peer_id, spewflag = self.spewflag, onstartfunc = self.OnStart)
#For profiling the DL code
#import hotshot
#def profit():
# self.download.download(self.params, self.OnUpdateStatus, self.OnFinished, self.OnError, self.FriendFunc,
# self.doneflag, 100, self.peer_id, spewflag = self.spewflag, onstartfunc = self.OnStart)
#prof = hotshot.Profile("download.prof")
#prof.runcall(profit)
#prof.close()
if not self.doneflag:
self.OnEnd(False)
return False
except:
print_exc()
self.OnEnd(True)
print 'download thread finished', self.id
return True
|