PageRenderTime 31ms CodeModel.GetById 11ms RepoModel.GetById 0ms app.codeStats 0ms

/worker.py

https://bitbucket.org/xplugins/download-manager
Python | 386 lines | 361 code | 8 blank | 17 comment | 1 complexity | 666edbb5ed27c9d641a10967d57d85f6 MD5 | raw file
Possible License(s): GPL-3.0
  1. # Download Manager - A utility for queing and downloading very larg files over http.
  2. # Copyright (C) 2009-2011 Ben Russell, br@x-plugins.com
  3. #
  4. # This program is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation, either version 3 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. import urllib
  17. import urllib2
  18. import os
  19. import time
  20. #import sys
  21. import threading
  22. import string
  23. import socket
  24. from worker_fetchfile import Worker_FetchFile
  25. #this is the background work thread interface that the GUI talks to.
  26. class WorkerPool( threading.Thread ):
  27. def __init__(self):
  28. threading.Thread.__init__(self)
  29. self.outputFolder = ""
  30. self.timeToQuit = threading.Event()
  31. self.timeToQuit.clear()
  32. #10 second time out on Socket based operations.
  33. timeout = 10
  34. socket.setdefaulttimeout( timeout )
  35. self.fetch_pool = []
  36. self.max_concurrent_transfers = 2
  37. self.total_bytesPerSecond = 0
  38. self.total_bytesInPool = 0
  39. self.total_megabytesInPool = 0.0
  40. self.throttle_target = 89
  41. self.throttle = 75
  42. self.worker_block_size = 8192
  43. self.eta = "..."
  44. #these are broken down to represent the entire duration, none of them is a sum total
  45. self.last_eta_minutes = 0.0
  46. self.last_eta_hours = 0.0
  47. self.last_eta_days = 0.0
  48. #this is used to keep a total of the minutes require for this xfer. we then use this to build the "total ETA"
  49. #used to display progress of the entire package.
  50. self.eta_minutes_total = 0.0
  51. self.username = ""
  52. self.password = ""
  53. self.download_key = ""
  54. self.valid_key = False #this will be set if we manage to download a valid TOC
  55. self.mirror_id = -1
  56. self.mirror = ""
  57. self.package_name = ""
  58. self.package_info = ""
  59. self.package_urls_raw = []
  60. self.transfer_active = False
  61. def log(self,msg):
  62. #pass
  63. print(msg)
  64. """
  65. This function downloads the table of contents for a given purchase key.
  66. Key may be "demo" or an MD5 hash.
  67. """
  68. def download_package_info(self, package_name="demo"):
  69. import hashlib
  70. keylength = len(self.download_key)
  71. if( keylength != len(hashlib.md5("foo").hexdigest()) and keylength != len("demo") ):
  72. return
  73. #TODO: Make this configurable from a global config file for the app.
  74. base_url = "http://x-aviation.com/RealScenery10x/"
  75. params = urllib.urlencode( {'u':self.username, 'p':self.password, 'k':self.download_key, 'm':self.mirror} )
  76. #self.log(params)
  77. request_url = "%s?%s" %(base_url, params)
  78. self.valid_key = False # We don't know if the key is valid until we have asked the server.
  79. self.log("Requesting TOC: %s" %(request_url))
  80. try:
  81. fh = urllib2.urlopen( request_url )
  82. data = fh.read()
  83. fh.close()
  84. #FIXME: Parse the recieved TOC data and ensure that it looks valid.
  85. #bug: https://bitbucket.org/xplugins/download-manager/issue/4
  86. self.valid_key = True
  87. self.log("TOC Data:\n%s" %(data))
  88. except:
  89. self.log("Failed to get TOC data.")
  90. pass
  91. return
  92. lines = string.split(data,"\n")
  93. if( len(lines) > 1 ):
  94. self.package_name_raw = lines[0] # The first line contains package summary data.
  95. self.package_urls_raw = lines[1: ] # All other lines are assets to download.
  96. tokens = string.split(self.package_name_raw,"::")
  97. self.package_name = tokens[0]
  98. self.package_info = tokens[1]
  99. if( self.package_name == "" ):
  100. self.log("Error: Package name is blank.")
  101. return
  102. """
  103. This file downloads an asset from a scenery package, details are specified by the TOC data.
  104. """
  105. def download_package(self, package_name="demo"):
  106. self.transfer_active = True
  107. if( len(self.package_urls_raw) == 0 ):
  108. self.download_package_info(package_name=package_name)
  109. # Filtering and collection of the url list is done in the function download_package_info
  110. params = urllib.urlencode( {'u':self.username, 'p':self.password, 'k':self.download_key, 'm':self.mirror} )
  111. for r_u in self.package_urls_raw:
  112. if( r_u != "" ):
  113. tokens = string.split(r_u,"::")
  114. fetch_url = tokens[0]
  115. manifest_size = int(tokens[1])
  116. url_tokens = string.split(fetch_url, "/")
  117. filename = url_tokens[-1]
  118. full_output_file_path = "%s%s/%s" %(self.outputFolder, self.package_name, filename)
  119. package_label = "%s: %s" %(self.package_name, filename)
  120. tmp_url = "%s?%s" %(fetch_url, params)
  121. #FIXME: This code is hackish and ugly.
  122. # False filter to speed testing - allows downloading only small test files contained inside a well formed TOC.
  123. #if( fetch_url.endswith(".txt") or fetch_url.endswith("AZ.zip") ):
  124. #if( fetch_url.endswith(".txt") ):
  125. if( True ): #all files!
  126. self.queue_url(gui_label=package_label, url=tmp_url, outfile=full_output_file_path, manifest_size=manifest_size)
  127. """
  128. This function iterates the worker pool and calculates the active transfer rate for each item.
  129. Interval is allowed as an argument to allow control from our parent which will likely be a GUI or a thread.
  130. """
  131. def calculate_speed(self, interval):
  132. self.eta_minutes_total = 0.0 #reset accumulator
  133. mult = 1.0 / interval
  134. for worker in self.fetch_pool:
  135. #self.log('brx: %i\nlrx: %i' %(worker.bytesRecieved, worker.last_bytesRecieved))
  136. #d = (worker.bytesRecieved - worker.last_bytesRecieved)
  137. #self.log('d: %i' %(d))
  138. bytes_delta = worker.bytesExpected - worker.sock_byte_count
  139. worker.bytesPerSecond = (worker.bytesRecieved - worker.last_bytesRecieved) * mult
  140. worker.last_bytesRecieved = worker.bytesRecieved
  141. worker.speed = "%0.2f" %(worker.bytesPerSecond / 1024.0)
  142. eta_minutes = 0.0
  143. try:
  144. eta_minutes = (bytes_delta / worker.bytesPerSecond) / 60.0
  145. self.eta_minutes_total += eta_minutes
  146. eta_string = "..."
  147. if( eta_minutes <= 60.0 ):
  148. eta_string = "%.2f mins" %((eta_minutes+self.last_eta_minutes)/2.0)
  149. self.last_eta_minutes = eta_minutes
  150. else:
  151. eta_hours = eta_minutes / 60.0
  152. if( eta_hours <= 24.0 ):
  153. eta_string = "%.2f hrs" %((eta_hours+self.last_eta_hours)/2.0)
  154. self.last_eta_hours = eta_hours
  155. else:
  156. eta_days = eta_hours / 24.0
  157. eta_string = "%.2f days" %((eta_days+self.last_eta_days)/2.0)
  158. self.last_eta_days = eta_days
  159. if( worker.failed ):
  160. worker.eta = ""
  161. else:
  162. worker.eta = eta_string
  163. except:
  164. #FIXME: Log info here or really don't care ever? Clarify.
  165. pass
  166. def queue_url(self, url, outfile=None, params=None, post_data=None, gui_label="", manifest_size=-1):
  167. if( url == "" ): return
  168. for w in self.fetch_pool:
  169. if w.url == url: #and w.active == True:
  170. self.log("We already have a thread dealing with that URL...ignored.")
  171. return
  172. fetcher = Worker_FetchFile(url=url, outfile=outfile, gui_label=gui_label, manifest_size=manifest_size, parent=self)
  173. self.fetch_pool.append( fetcher )
  174. self.log("QD: %s" %(url))
  175. def run(self):
  176. #This loop controls start/stop of workers fetching content as well as servicing active workers.
  177. #It reads the user-facing sleep timers and adapts to control how much b/width we chew.
  178. self.active_count = 0 #active downloaders count
  179. while(True):
  180. if( self.throttle < self.throttle_target ):
  181. self.throttle += 2
  182. if( self.throttle > self.throttle_target ):
  183. self.throttle -= 2
  184. if( self.timeToQuit.isSet() ):
  185. break
  186. started_count = 0 #how many have we started in this run? - currently we only start one per run.
  187. destroy_pool = [] #collect items that are finished and clean them up, multi pass, 1st; find, 2nd; delete
  188. self.total_bytesPerSecond_tmp = 0 #stats accumulator
  189. self.total_bytesInPool = 0
  190. #asses number of active workers.
  191. self.active_count = 0
  192. for worker in self.fetch_pool:
  193. if( worker.active ):
  194. self.active_count += 1
  195. #service active workers.
  196. for worker in self.fetch_pool:
  197. #we limit how many new transfers can be started in one loop iteration so that we give more time to servicing existing connections
  198. if( worker.wants_start ):# and len(self.fetch_pool) < self.max_concurrent_transfers):
  199. if( started_count == 0 ):
  200. #spawning a new transfer.
  201. if( self.active_count < self.max_concurrent_transfers ):
  202. self.log("\nStarting a new transfer: %s" %(worker.url))
  203. worker.begin_transfer()
  204. self.active_count += 1
  205. started_count += 1
  206. else:
  207. self.total_bytesInPool += worker.bytesExpected
  208. #accumulate stats
  209. #service existing transfers
  210. if( worker.active ):
  211. #do transfer for recieved data, will write to disk as needed.
  212. worker.do_transfer()
  213. #adjust worker settings according to users interactive widgets.
  214. worker.block_size = self.worker_block_size
  215. self.total_bytesPerSecond_tmp += worker.bytesPerSecond
  216. self.total_bytesInPool += worker.bytesExpected
  217. # ------------ old and dusty comments, no idea how relevant. ---------
  218. #Worker has finished, add to clean up pool.
  219. #if( worker.finished ):
  220. # self.log("Tidy up\n\n\n")
  221. # worker.finish_transfer()
  222. # self.active_count -= 1
  223. # if( worker.payload != "" ):
  224. # worker.payload = ""
  225. #
  226. # destroy_pool.append( worker )
  227. #end of worker pool loop
  228. #self.clean_pool()
  229. # ------------ end old and dusty comments, no idea how relevant. ---------
  230. self.total_bytesPerSecond = self.total_bytesPerSecond_tmp
  231. # ------- code below here is hack-ish throttle tuned code to adjust timing off service loops and block sizes, just dont go there. ----
  232. #sleep delay is tuned. dont change it unless you want to change the slider as well.
  233. throttle_p = self.throttle / 100.0
  234. if( throttle_p >= 0.9 ):
  235. throttle_p = 1.0 #mac slider bug work around.
  236. inv_throttle_p = 1.0 - throttle_p
  237. new_block_size = int(8192.0 * throttle_p)
  238. if( new_block_size < 1 ):
  239. new_block_size = 1
  240. sleep_delay = 1.0 * (inv_throttle_p)
  241. if( sleep_delay < 0.001 ):
  242. sleep_delay = 0.001
  243. self.total_megabytesInPool = self.megabytes( self.total_bytesInPool )
  244. self.worker_block_size = new_block_size
  245. time.sleep(sleep_delay)
  246. #TODO: Scan code to see if this function is still used.
  247. def clean_pool(self):
  248. pass
  249. #We no longer clean these out, leaving them in leaves them in the GUI and provides the user with more feedback on download state for free.
  250. #Iterate clean up pool and destroy finished workers.
  251. #for target in destroy_pool:
  252. # self.fetch_pool.remove(target)
  253. def stop(self):
  254. self.timeToQuit.set()
  255. # Utility funciton, move to shared module.
  256. def megabytes( self, value ):
  257. return float(value) / 1048576.0