home *** CD-ROM | disk | FTP | other *** search
/ Chip 2006 June / CHIP 2006-06.2.iso / program / freeware / Democracy-0.8.2.exe / xulrunner / python / dl_daemon / download.py < prev   
Encoding:
Python Source  |  2006-04-10  |  23.2 KB  |  684 lines

  1. import os
  2. import sys
  3. import bsddb
  4. import random
  5. import types
  6. from os import remove, rename, access, F_OK
  7. from threading import RLock, Event, Thread
  8. from time import sleep, time
  9. from copy import copy
  10.  
  11. from download_utils import grabURL, cleanFilename, parseURL
  12.  
  13. import config
  14. from BitTorrent import configfile
  15. from BitTorrent.download import Feedback, Multitorrent
  16. from BitTorrent.defaultargs import get_defaults
  17. from BitTorrent.parseargs import parseargs, printHelp
  18. from BitTorrent.bencode import bdecode
  19. from BitTorrent.ConvertedMetainfo import ConvertedMetainfo
  20. from BitTorrent import configfile
  21. from BitTorrent import BTFailure, CRITICAL
  22. from BitTorrent import version
  23.  
  24. from dl_daemon import command, daemon
  25.  
  26. # a hash of download ids to downloaders
  27. _downloads = {}
  28. # a hash of URLs to downloaders
  29. _downloads_by_url = {}
  30.  
  31. defaults = get_defaults('btdownloadheadless')
  32. defaults.extend((('donated', '', ''),))
  33.  
  34. #FIXME: check for free space and failed connection to tracker and fail
  35. #on those cases
  36.  
  37. #FIXME: Trigger pending Manual Download in item when if we run out of disk space
  38.  
  39. _lock = RLock()
  40.  
  41. def findHTTPAuth(*args, **kws):
  42.     x = command.FindHTTPAuthCommand(daemon.lastDaemon, *args, **kws)
  43.     return x.send(block = True, retry = True)
  44.  
  45. def generateDownloadID():
  46.     _lock.acquire()
  47.     try:
  48.         dlid = "download%08d" % random.randint(0,99999999)
  49.         while _downloads.has_key(dlid):
  50.             dlid = "download%08d" % random.randint(0,99999999)
  51.     finally:
  52.         _lock.release()
  53.     return dlid
  54.  
  55. def createDownloader(url, contentType, dlid):
  56.     if contentType == 'application/x-bittorrent':
  57.         return BTDownloader(url, dlid)
  58.     else:
  59.         return HTTPDownloader(url, dlid)
  60.  
  61. # Creates a new downloader object. Returns id on success, None on failure
  62. def startNewDownload(url, contentType):
  63.     _lock.acquire()
  64.     try:
  65.         if _downloads_by_url.has_key(url):
  66.             dlid = _downloads_by_url[url].dlid
  67.         else:
  68.             dlid = generateDownloadID()
  69.             dl = createDownloader(url, contentType, dlid)
  70.             _downloads[dlid] = dl
  71.             _downloads_by_url[url] = dl
  72.     finally:
  73.         _lock.release()
  74.         return dlid
  75.  
  76. def pauseDownload(dlid):
  77.     try:
  78.         download = _downloads[dlid]
  79.     except: # There is no download with this id
  80.         return True
  81.     return download.pause()
  82.  
  83. def startDownload(dlid):
  84.     try:
  85.         download = _downloads[dlid]
  86.     except: # There is no download with this id
  87.         return True
  88.     return download.start()
  89.  
  90. def stopDownload(dlid):
  91.     try:
  92.         _lock.acquire()
  93.         try:
  94.             download = _downloads[dlid]
  95.             del _downloads[dlid]
  96.             # A download may be referred to by multiple ids
  97.             if not download in _downloads:
  98.                 del _downloads_by_url[download.url]
  99.         finally:
  100.             _lock.release()
  101.     except: # There is no download with this id
  102.         return True
  103.     return download.stop()
  104.  
  105. def getDownloadStatus(dlids = None):
  106.     statuses = {}
  107.     for key in _downloads.keys():
  108.         if ((dlids is None)  or (dlids == key) or (key in dlids)):
  109.             try:
  110.                 statuses[key] = _downloads[key].getStatus()
  111.             except:
  112.                 pass
  113.     return statuses
  114.  
  115. def shutDown():
  116.     print "Shutting down downloaders..."
  117.     for dlid in _downloads:
  118.         _downloads[dlid].pause()
  119.     shutdownBTDownloader()
  120.  
  121. def restoreDownloader(downloader):
  122.     # changes to the downloader's dict shouldn't affect this
  123.     downloader = copy(downloader)
  124.  
  125.     dlerType = downloader.get('dlerType')
  126.     if dlerType == 'HTTP':
  127.         dl = HTTPDownloader(restore = downloader)
  128.     elif dlerType == 'BitTorrent':
  129.         dl = BTDownloader(restore = downloader)
  130.     else:
  131.         print "WARNING dlerType %s not recognized" % dlerType
  132.         dl = createDownloader(downloader['url'], downloader['contentType'],
  133.                 downloader['dlid'])
  134.         print "created new downloader: %s" % dl
  135.  
  136.     _downloads[downloader['dlid']] = dl
  137.     _downloads_by_url[downloader['url']] = dl
  138.  
  139. class BGDownloader:
  140.     def __init__(self, url, dlid):
  141.         self.dlid = dlid
  142.         self.url = url
  143.         self.startTime = time()
  144.         self.endTime = self.startTime
  145.         self.shortFilename = self.filenameFromURL(url)
  146.         self.pickInitialFilename()
  147.         self.state = "downloading"
  148.         self.currentSize = 0
  149.         self.totalSize = -1
  150.         self.blockTimes = []
  151.         self.reasonFailed = "No Error"
  152.         self.headers = None
  153.         self.thread = Thread(target=self.runDownloader, \
  154.                              name="downloader -- %s" % self.shortFilename)
  155.         self.thread.setDaemon(False)
  156.         self.thread.start()
  157.  
  158.     def getURL(self):
  159.         return self.url
  160.  
  161.     def getStatus(self):
  162.         return {'dlid': self.dlid,
  163.             'url': self.url,
  164.             'state': self.state,
  165.             'totalSize': self.totalSize,
  166.             'currentSize': self.currentSize,
  167.             'eta': self.getETA(),
  168.             'rate': self.getRate(),
  169.             'uploaded': 0,
  170.             'filename': self.filename,
  171.             'shortFilename': self.shortFilename,
  172.             'reasonFailed': self.reasonFailed,
  173.             'dlerType': None }
  174.  
  175.     def updateClient(self):
  176.         x = command.UpdateDownloadStatus(daemon.lastDaemon, self.getStatus())
  177.         return x.send(block = False, retry = False)
  178.         
  179.     ##
  180.     # Returns a reasonable filename for saving the given url
  181.     def filenameFromURL(self,url):
  182.         (scheme, host, path, params, query, fragment) = parseURL(url)
  183.         if len(path):
  184.             try:
  185.                 ret = re.compile("^.*?([^/]+)/?$").search(path).expand("\\1")
  186.                 return cleanFilename(ret)
  187.  
  188.             except:
  189.                 return 'unknown'
  190.         else:
  191.             return "unknown"
  192.  
  193.     ##
  194.     # Finds a filename that's unused and similar the the file we want
  195.     # to download
  196.     def nextFreeFilename(self, name):
  197.         if not access(name,F_OK):
  198.             return name
  199.         parts = name.split('.')
  200.         count = 1
  201.         if len(parts) == 1:
  202.             newname = "%s.%s" % (name, count)
  203.             while access(newname,F_OK):
  204.                 count += 1
  205.                 newname = "%s.%s" % (name, count)
  206.         else:
  207.             insertPoint = len(parts)-1
  208.             parts[insertPoint:insertPoint] = [str(count)]
  209.             newname = '.'.join(parts)
  210.             while access(newname,F_OK):
  211.                 count += 1
  212.                 parts[insertPoint] = str(count)
  213.                 newname = '.'.join(parts)
  214.         return newname
  215.  
  216.     def pickInitialFilename(self):
  217.         """Pick a path to download to based on self.shortFilename.
  218.  
  219.         This method sets self.filename, as well as creates any leading paths
  220.         needed to start downloading there.
  221.         """
  222.  
  223.         downloadDir = os.path.join(config.get(config.MOVIES_DIRECTORY),
  224.                 'Incomplete Downloads')
  225.         # Create the download directory if it doesn't already exist.
  226.         try:
  227.             os.makedirs(downloadDir)
  228.         except:
  229.             pass
  230.         baseFilename = os.path.join(downloadDir, self.shortFilename+".part")
  231.         self.filename = self.nextFreeFilename(baseFilename)
  232.  
  233.     ##
  234.     # Returns a float with the estimated number of seconds left
  235.     def getETA(self):
  236.         rate = self.getRate()
  237.         if rate > 0:
  238.             return (self.totalSize - self.currentSize)/rate
  239.         else:
  240.             return 0
  241.  
  242.     ##
  243.     # Returns a float with the download rate in bytes per second
  244.     def getRate(self):
  245.         now = time()
  246.         if self.endTime != self.startTime:
  247.             rate = self.currentSize/(self.endTime-self.startTime)
  248.         else:
  249.             try:
  250.                 if (now-self.blockTimes[0][0]) != 0:
  251.                     rate=(self.blockTimes[-1][1]-self.blockTimes[0][1])/(now-self.blockTimes[0][0])
  252.                 else:
  253.                     rate = 0
  254.             except IndexError:
  255.                 rate = 0
  256.         return rate
  257.  
  258. class HTTPDownloader(BGDownloader):
  259.     def __init__(self, url = None,dlid = None,restore = None):
  260.         if restore is not None:
  261.             self.restoreState(restore)
  262.         else:
  263.             self.lastUpdated = 0
  264.             BGDownloader.__init__(self,url, dlid)
  265.  
  266.     def getStatus(self):
  267.         data = BGDownloader.getStatus(self)
  268.         data['lastUpdated'] = self.lastUpdated
  269.         data['dlerType'] = 'HTTP'
  270.         return data
  271.  
  272.     ##
  273.     # Update the download rate and eta based on recieving length bytes
  274.     def updateRateAndETA(self,length):
  275.         now = time()
  276.         updated = False
  277.         self.currentSize = self.currentSize + length
  278.         if self.lastUpdated < now-3:
  279.             self.blockTimes.append((now,  self.currentSize))
  280.             #Only keep the last 100 packets
  281.             if len(self.blockTimes)>100:
  282.                 self.blockTimes.pop(0)
  283.             updated = True
  284.             self.lastUpdated = now
  285.         if updated:
  286.             self.updateClient()
  287.         
  288.     ##
  289.     # Grabs the next block from the HTTP connection
  290.     def getNextBlock(self,handle):
  291.         state = self.state
  292.         if (state == "paused") or (state == "stopped"):
  293.             data = ""
  294.         else:
  295.             try:
  296.                 data = handle.read(1024)
  297.             except:
  298.                 self.state = "failed"
  299.                 self.reasonFailed = "Lost connection to server"
  300.                 data = ""
  301.         self.updateRateAndETA(len(data))
  302.         return data
  303.  
  304.     ##
  305.     # This is the actual download thread.
  306.     def runDownloader(self, retry = False):
  307.         if retry:
  308.             pos = self.currentSize
  309.             info = grabURL(self.url,"GET",pos, findHTTPAuth = findHTTPAuth)
  310.             if info is None and pos > 0:
  311.                 pos = 0
  312.                 self.currentSize = 0
  313.                 info = grabURL(self.url,"GET", findHTTPAuth = findHTTPAuth)
  314.             if info is None:
  315.                 self.state = "failed"
  316.                 self.reasonFailed = "Could not connect to server"
  317.                 return False
  318.             try:
  319.                 filehandle = file(self.filename,"r+b")
  320.                 filehandle.seek(pos)
  321.             except:
  322.                 #the file doesn't exist. Get the right filename and restart dl
  323.                 self.shortFilename = cleanFilename(info['filename'])
  324.                 self.pickInitialFilename()
  325.                 filehandle = file(self.filename,"w+b")
  326.                 self.currentSize = 0
  327.                 totalSize = self.totalSize
  328.                 pos = 0
  329.                 if totalSize > 0:
  330.                     filehandle.seek(totalSize-1)
  331.                     filehandle.write(' ')
  332.                     filehandle.seek(0)            
  333.         else:
  334.             #print "We don't have any INFO..."
  335.             info = grabURL(self.url,"GET", findHTTPAuth = findHTTPAuth)
  336.             if info is None:
  337.                 self.state = "failed"
  338.                 self.reasonFailed = "Could not connect to server"
  339.                 return False
  340.  
  341.         if not retry:
  342.             #get the filename to save to
  343.             self.shortFilename = cleanFilename(info['filename'])
  344.             self.pickInitialFilename()
  345.  
  346.             #Get the length of the file, then create it
  347.             try:
  348.                 totalSize = int(info['content-length'])
  349.             except KeyError:
  350.                 totalSize = -1
  351.             self.totalSize = totalSize
  352.             try:
  353.                 filehandle = file(self.filename,"w+b")
  354.             except IOError:
  355.                 self.state = "failed"
  356.                 self.reasonFailed = "Could not write file to disk"
  357.                 return False
  358.             self.currentSize = 0
  359.             if not self.acceptDownloadSize(totalSize):
  360.                 self.state = "failed"
  361.                 self.reasonFailed = "Not enough free space"
  362.                 return False
  363.             pos = 0
  364.             if totalSize > 0:
  365.                 filehandle.seek(totalSize-1)
  366.                 filehandle.write(' ')
  367.                 filehandle.seek(0)
  368.  
  369.         #Download the file
  370.         if pos != self.totalSize:
  371.             data = self.getNextBlock(info['file-handle'])
  372.             while len(data) > 0:
  373.                 filehandle.write(data)
  374.                 data = self.getNextBlock(info['file-handle'])
  375.             filehandle.close()
  376.             info['file-handle'].kill()
  377.  
  378.         #Update the status
  379.         if self.state == "downloading":
  380.             self.state = "finished"
  381.             newfilename = os.path.join(config.get(config.MOVIES_DIRECTORY),self.shortFilename)
  382.             newfilename = self.nextFreeFilename(newfilename)
  383.             try:
  384.                 rename(self.filename,newfilename)
  385.                 self.filename = newfilename
  386.             except:
  387.                 # Eventually we should make this bring up an error
  388.                 # dialog in the app
  389.                 print "Democracy: Warning: Couldn't rename \"%s\" to \"%s\"" %(
  390.                     self.filename, newfilename)
  391.             if self.totalSize == -1:
  392.                 self.totalSize = self.currentSize
  393.             self.endTime = time()
  394.             self.state = "finished"
  395.         elif self.state == "stopped":
  396.             try:
  397.                 remove(self.filename)
  398.             except:
  399.                 pass
  400.         self.updateClient()
  401.  
  402.     ##
  403.     # Checks the download file size to see if we can accept it based on the 
  404.     # user disk space preservation preference
  405.     def acceptDownloadSize(self, size):
  406.         print "WARNING: acceptDownloadSize is a stub"
  407.         return True
  408.         if config.get(config.PRESERVE_DISK_SPACE):
  409.             sizeInGB = size / 1024 / 1024 / 1024
  410.             if sizeInGB > platformutils.getAvailableGBytesForMovies() - config.get(config.PRESERVE_X_GB_FREE):
  411.                 self.state = "failed"
  412.                 self.reasonFailed = "File is too big"
  413.                 return False
  414.         return True
  415.  
  416.     ##
  417.     # Pauses the download.
  418.     def pause(self):
  419.         if self.state != "stopped":
  420.             self.state = "paused"
  421.             self.updateClient()
  422.  
  423.     ##
  424.     # Stops the download and removes the partially downloaded
  425.     # file.
  426.     def stop(self):
  427.         if self.state != "downloading":
  428.             try:
  429.                 remove(self.filename)
  430.             except:
  431.                 pass
  432.         self.state = "stopped"
  433.         self.updateClient()
  434.         #FIXME: remove downloader from memory
  435.  
  436.     ##
  437.     # Continues a paused or stopped download thread
  438.     def start(self):
  439.         self.state = "downloading"
  440.         self.updateClient()
  441.         print "Warning starting downloader in thread"
  442.         self.runDownloader(True)
  443.  
  444.     def restoreState(self, data):
  445.         self.__dict__ = copy(data)
  446.         if self.state == "downloading":
  447.             self.thread = Thread(target=lambda:self.runDownloader(retry = True), \
  448.                                  name="downloader -- %s" % self.shortFilename)
  449.             self.thread.setDaemon(False)
  450.             self.thread.start()
  451.  
  452.  
  453. ##
  454. # BitTorrent uses this class to display status information. We use
  455. # it to update Downloader information
  456. #
  457. # We use the rate and ETA provided by BitTorrent rather than
  458. # calculating our own.
  459. class BTDisplay:
  460.     ##
  461.     # Takes in the downloader class associated with this display
  462.     def __init__(self,dler):
  463.         self.dler = dler
  464.         self.lastUpTotal = 0
  465.         self.lastUpdated = 0
  466.  
  467.     def finished(self):
  468.         self.dler.updateClient()
  469.         state = self.dler.state
  470.         if not (state == "uploading" or
  471.                 state == "finished"):
  472.             self.dler.state = "uploading"
  473.             newfilename = os.path.join(config.get(config.MOVIES_DIRECTORY),self.dler.shortFilename)
  474.             newfilename = self.dler.nextFreeFilename(newfilename)
  475.             rename(self.dler.filename,newfilename)
  476.             self.dler.filename = newfilename
  477.             self.dler.endTime = time()
  478.             if self.dler.endTime - self.dler.startTime != 0:
  479.                 self.dler.rate = self.dler.totalSize/(self.dler.endTime-self.dler.startTime)
  480.             self.dler.currentSize =self.dler.totalSize
  481.             self.dler.multitorrent.singleport_listener.remove_torrent(self.dler.metainfo.infohash)
  482.             self.dler.torrent = self.dler.multitorrent.start_torrent(self.dler.metainfo,self.dler.torrentConfig, self.dler, self.dler.filename)
  483.  
  484.         self.dler.updateClient()
  485.  
  486.     def error(self, errormsg):
  487.         print errormsg
  488.             
  489.     def display(self, statistics):
  490.         update = False
  491.         now = time()
  492.         if statistics.get('upTotal') != None:
  493.             if self.lastUpTotal > statistics.get('upTotal'):
  494.                 self.dler.uploaded += statistics.get('upTotal')
  495.             else:
  496.                 self.dler.uploaded += statistics.get('upTotal') - self.lastUpTotal
  497.             self.lastUpTotal = statistics.get('upTotal')
  498.         if self.dler.state != "paused":
  499.             self.dler.currentSize = int(self.dler.totalSize*statistics.get('fractionDone'))
  500.         if self.dler.state != "finished" and self.dler.state != "uploading":
  501.             self.dler.rate = statistics.get('downRate')
  502.         if self.dler.rate == None:
  503.             self.dler.rate = 0.0
  504.         self.dler.eta = statistics.get('timeEst')
  505.         if self.dler.eta == None:
  506.             self.dler.eta = 0
  507.         if (self.dler.state == "uploading" and
  508.             self.dler.uploaded >= 1.5*self.dler.totalSize):
  509.             self.dler.state = "finished"
  510.             self.dler.torrent.shutdown()
  511.         if self.lastUpdated < now-3:
  512.             update = True
  513.             self.lastUpdated = now
  514.         if update:
  515.             self.dler.updateClient()
  516.  
  517. class BTDownloader(BGDownloader):
  518.     def global_error(level, text):
  519.         print "Bittorrent error (%s): %s" % (level, text)
  520.     doneflag = Event()
  521.     torrentConfig = configfile.parse_configuration_and_args(defaults,'btdownloadheadless', [], 0, None)
  522.     torrentConfig = torrentConfig[0]
  523.     multitorrent = Multitorrent(torrentConfig, doneflag, global_error)
  524.  
  525.     def __init__(self, url = None, item = None, restore = None):
  526.         self.metainfo = None
  527.         self.rate = 0
  528.         self.eta = 0
  529.         self.d = BTDisplay(self)
  530.         self.uploaded = 0
  531.         self.torrent = None
  532.         if restore is not None:
  533.             self.restoreState(restore)
  534.         else:            
  535.             BGDownloader.__init__(self,url,item)
  536.  
  537.     def restoreState(self, data):
  538.         self.__dict__ = data
  539.         self.d = BTDisplay(self)
  540.         if self.state in ("downloading","uploading"):
  541.             self.thread = Thread(target=self.restartDL, \
  542.                                  name="downloader -- %s" % self.shortFilename)
  543.             self.thread.setDaemon(False)
  544.             self.thread.start()
  545.  
  546.     def getStatus(self):
  547.         data = BGDownloader.getStatus(self)
  548.         data['metainfo'] = self.metainfo
  549.         data['dlerType'] = 'BitTorrent'
  550.         return data
  551.  
  552.     def getRate(self):
  553.         return self.rate
  554.  
  555.     def getETA(self):
  556.         return self.eta
  557.         
  558.     def pause(self):
  559.         self.state = "paused"
  560.         self.updateClient()
  561.         try:
  562.             self.torrent.shutdown()
  563.         except KeyError:
  564.             pass
  565.         except AttributeError:
  566.             pass
  567.  
  568.     def stop(self):
  569.         self.state = "stopped"
  570.         self.updateClient()
  571.         if self.torrent is not None:
  572.             self.torrent.shutdown()
  573.             try:
  574.                 self.torrent.shutdown()
  575.             except KeyError:
  576.                 pass
  577.         try:
  578.             remove(self.filename)
  579.         except:
  580.             pass
  581.  
  582.     def start(self):
  583.         self.pause()
  584.         metainfo = self.metainfo
  585.         if metainfo == None:
  586.             self.reasonFailed = "Could not read BitTorrent metadata"
  587.             self.state = "failed"
  588.         else:
  589.             self.state = "downloading"
  590.         self.updateClient()
  591.         if metainfo != None:
  592.             self.torrent = self.multitorrent.start_torrent(metainfo,
  593.                                 self.torrentConfig, self, self.filename)
  594.  
  595.     def runDownloader(self,done=False):
  596.         self.updateClient()
  597.         if self.metainfo is None:
  598.             h = grabURL(self.getURL(),"GET", findHTTPAuth = findHTTPAuth)
  599.             if h is None:
  600.                 self.state = "failed"
  601.                 self.reasonFailed = "Could not connect to server"
  602.                 self.updateClient()
  603.                 return
  604.             else:
  605.                 metainfo = h['file-handle'].read()
  606.                 h['file-handle'].close()
  607.         try:
  608.             # raises BTFailure if bad
  609.             if self.metainfo is None:
  610.                 metainfo = ConvertedMetainfo(bdecode(metainfo))
  611.             else:
  612.                 metainfo = self.metainfo
  613.             self.shortFilename = metainfo.name_fs
  614.             if not done:
  615.                 self.pickInitialFilename()
  616.             if self.metainfo is None:
  617.                 self.metainfo = metainfo
  618.             self.set_torrent_values(self.metainfo.name, self.filename,
  619.                                 self.metainfo.total_bytes, len(self.metainfo.hashes))
  620.             self.torrent = self.multitorrent.start_torrent(self.metainfo,
  621.                                 self.torrentConfig, self, self.filename)
  622.         except BTFailure, e:
  623.             print str(e)
  624.             return
  625.         self.get_status()
  626.  
  627.     ##
  628.     # Functions below this point are needed by BitTorrent
  629.     def set_torrent_values(self, name, path, size, numpieces):
  630.         self.totalSize = size
  631.  
  632.     def exception(self, torrent, text):
  633.         self.error(torrent, CRITICAL, text)
  634.  
  635.     def started(self, torrent):
  636.         pass
  637.  
  638.  
  639.     def get_status(self):
  640.         #print str(self.getID()) + ": "+str(self.metainfo.infohash).encode('hex')
  641.         self.multitorrent.rawserver.add_task(self.get_status,
  642.                                              self.torrentConfig['display_interval'])
  643.         status = self.torrent.get_status(False)
  644.         self.d.display(status)
  645.  
  646.     def error(self, torrent, level, text):
  647.         self.d.error(text)
  648.  
  649.     def failed(self, torrent, is_external):
  650.         pass
  651.  
  652.     def finished(self, torrent):
  653.         self.d.finished()
  654.  
  655.     def restartDL(self):
  656.         if self.metainfo != None and self.state != "finished":
  657.             self.torrent = self.multitorrent.start_torrent(self.metainfo,
  658.                                       self.torrentConfig, self, self.filename)
  659.  
  660.             self.get_status()
  661.         elif self.state != "finished":
  662.             self.state = "paused"
  663.  
  664.     @classmethod
  665.     def wakeup(self):
  666.         if sys.platform != 'win32':
  667.             if BTDownloader.multitorrent.rawserver.wakeupfds[1] is not None:
  668.                 os.write(BTDownloader.multitorrent.rawserver.wakeupfds[1], 'X')
  669.  
  670. ##
  671. # Kill the main BitTorrent thread
  672. #
  673. # This should be called before closing the app
  674. def shutdownBTDownloader():
  675.     BTDownloader.doneflag.set()
  676.     BTDownloader.wakeup()
  677.     BTDownloader.dlthread.join()
  678.  
  679. def startBTDownloader():
  680.     #Spawn the download thread
  681.     BTDownloader.dlthread = Thread(target=BTDownloader.multitorrent.rawserver.listen_forever)
  682.     BTDownloader.dlthread.setName("bittorrent downloader")
  683.     BTDownloader.dlthread.start()
  684.