1
0

MultiTorrent.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856
  1. # The contents of this file are subject to the BitTorrent Open Source License
  2. # Version 1.0 (the License). You may not copy or use this file, in either
  3. # source code or executable form, except in compliance with the License. You
  4. # may obtain a copy of the License at http://www.bittorrent.com/license/.
  5. #
  6. # Software distributed under the License is distributed on an AS IS basis,
  7. # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  8. # for the specific language governing rights and limitations under the
  9. # License.
  10. # Author: Steve Hazel, Bram Cohen, and Uoti Urpala.
  11. import os
  12. import sys
  13. import shutil
  14. import socket
  15. import cPickle
  16. import logging
  17. import traceback
  18. from copy import copy
  19. from BTL.translation import _
  20. from BitTorrent.Choker import Choker
  21. from BTL.platform import bttime, encode_for_filesystem, get_filesystem_encoding
  22. from BitTorrent.platform import old_broken_config_subencoding
  23. from BitTorrent.Torrent import Feedback, Torrent
  24. from BTL.bencode import bdecode
  25. from BTL.ConvertedMetainfo import ConvertedMetainfo
  26. from BTL.exceptions import str_exc
  27. from BitTorrent.prefs import Preferences
  28. from BitTorrent.NatTraversal import NatTraverser
  29. from BitTorrent.BandwidthManager import BandwidthManager
  30. from BitTorrent.InternetWatcher import get_internet_watcher
  31. from BitTorrent.NewRateLimiter import MultiRateLimiter as RateLimiter
  32. from BitTorrent.DownloadRateLimiter import DownloadRateLimiter
  33. from BitTorrent.ConnectionManager import SingleportListener
  34. from BitTorrent.CurrentRateMeasure import Measure
  35. from BitTorrent.Storage import FilePool
  36. from BTL.yielddefer import launch_coroutine
  37. from BTL.defer import Deferred, DeferredEvent, wrap_task
  38. from BitTorrent import BTFailure, InfoHashType
  39. from BitTorrent import configfile
  40. from khashmir.utkhashmir import UTKhashmir
  41. class TorrentException(BTFailure):
  42. pass
  43. class TorrentAlreadyInQueue(TorrentException):
  44. pass
  45. class TorrentAlreadyRunning(TorrentException):
  46. pass
  47. class TorrentNotInitialized(TorrentException):
  48. pass
  49. class TorrentNotRunning(TorrentException):
  50. pass
  51. class UnknownInfohash(TorrentException):
  52. pass
  53. class TorrentShutdownFailed(TorrentException):
  54. pass
  55. class TooManyTorrents(TorrentException):
  56. pass
  57. #class DummyTorrent(object):
  58. # def __init__(self, infohash):
  59. # self.metainfo = object()
  60. # self.metainfo.infohash = infohash
  61. BUTLE_INTERVAL = 1
  62. class MultiTorrent(Feedback):
  63. """A MultiTorrent object represents a set of BitTorrent file transfers.
  64. It acts as a factory for Torrent objects, and it acts as
  65. the interface through which communication is performed to and from
  66. torrent file transfers.
  67. If you wish to instantiate MultiTorrent to download only a single
  68. torrent then pass is_single_torrent=True.
  69. If you want to avoid resuming from prior torrent config state then
  70. pass resume_from_torrent_config = False.
  71. It will still use fast resume if available.
  72. """
  73. def __init__(self, config, rawserver,
  74. data_dir, listen_fail_ok=False, init_torrents=True,
  75. is_single_torrent=False, resume_from_torrent_config=True):
  76. """
  77. @param config: program-wide configuration object.
  78. @param rawserver: object that manages main event loop and event
  79. scheduling.
  80. @param data_dir: where variable data such as fastresume information
  81. and GUI state is saved.
  82. @param listen_fail_ok: if false, a BTFailure is raised if
  83. a server socket cannot be opened to accept incoming peer
  84. connections.
  85. @param init_torrents: restore fast resume state from prior
  86. instantiations of MultiTorrent.
  87. @param is_single_torrent: if true then allow only one torrent
  88. at a time in this MultiTorrent.
  89. @param resume_from_torrent_config: resume from ui_state files.
  90. """
  91. # is_single_torrent will go away when we move MultiTorrent into
  92. # a separate process, in which case, single torrent applications like
  93. # curses and console will act as a client to the MultiTorrent daemon.
  94. # --Dave
  95. # init_torrents refers to fast resume rather than torrent config.
  96. # If init_torrents is set to False, the UI state file is still
  97. # read and the paths to existing downloads still used. This is
  98. # not what we want for launchmany.
  99. #
  100. # resume_from_torrent_config is separate from
  101. # is_single_torrent because launchmany must be able to have
  102. # multiple torrents while not resuming from torrent config
  103. # state. If launchmany resumes from torrent config then it
  104. # saves or seeds from the path in the torrent config even if
  105. # the file has moved in the directory tree. Because
  106. # launchmany has no mechanism for removing torrents other than
  107. # to change the directory tree, the only way for the user to
  108. # eliminate the old state is to wipe out the files in the
  109. # .bittorrent/launchmany-*/ui_state directory. This is highly
  110. # counterintuitive. Best to simply ignore the ui_state
  111. # directory altogether. --Dave
  112. assert isinstance(config, Preferences)
  113. #assert isinstance(data_dir, unicode) # temporarily commented -Dave
  114. assert isinstance(listen_fail_ok, bool)
  115. assert not (is_single_torrent and resume_from_torrent_config)
  116. # flag for done
  117. self.isDone = False
  118. self.config = config
  119. self.data_dir = data_dir
  120. self.last_save_time = 0
  121. self.policies = []
  122. self.torrents = {}
  123. self.running = {}
  124. self.log_root = "core.MultiTorrent"
  125. self.logger = logging.getLogger(self.log_root)
  126. self.is_single_torrent = is_single_torrent
  127. self.resume_from_torrent_config = resume_from_torrent_config
  128. self.auto_update_policy_index = None
  129. self.dht = None
  130. self.rawserver = rawserver
  131. nattraverser = NatTraverser(self.rawserver)
  132. self.internet_watcher = get_internet_watcher(self.rawserver)
  133. self.singleport_listener = SingleportListener(self.rawserver,
  134. nattraverser,
  135. self.log_root,
  136. config['use_local_discovery'])
  137. self.choker = Choker(self.config, self.rawserver.add_task)
  138. self.up_ratelimiter = RateLimiter(self.rawserver.add_task)
  139. self.up_ratelimiter.set_parameters(config['max_upload_rate'],
  140. config['upload_unit_size'])
  141. self.down_ratelimiter = DownloadRateLimiter(
  142. config['download_rate_limiter_interval'],
  143. self.config['max_download_rate'])
  144. self.total_downmeasure = Measure(config['max_rate_period'])
  145. self._find_port(listen_fail_ok)
  146. self.filepool_doneflag = DeferredEvent()
  147. self.filepool = FilePool(self.filepool_doneflag,
  148. self.rawserver.add_task,
  149. self.rawserver.external_add_task,
  150. config['max_files_open'],
  151. config['num_disk_threads'])
  152. if self.resume_from_torrent_config:
  153. try:
  154. self._restore_state(init_torrents)
  155. except BTFailure:
  156. # don't be retarted.
  157. self.logger.exception("_restore_state failed")
  158. def no_dump_set_option(option, value):
  159. self.set_option(option, value, dump=False)
  160. self.bandwidth_manager = BandwidthManager(
  161. self.rawserver.external_add_task, config,
  162. no_dump_set_option, self.rawserver.get_remote_endpoints,
  163. get_rates=self.get_total_rates )
  164. self.rawserver.add_task(0, self.butle)
  165. def butle(self):
  166. policy = None
  167. try:
  168. for policy in self.policies:
  169. policy.butle()
  170. except:
  171. # You had something to hide, should have hidden it shouldn't you?
  172. self.logger.error("Butler error", exc_info=sys.exc_info())
  173. # Should we remove policies?
  174. #if policy:
  175. # self.policies.remove(policy)
  176. self.rawserver.add_task(BUTLE_INTERVAL, self.butle)
  177. def _find_port(self, listen_fail_ok=True):
  178. """Run BitTorrent on the first available port found starting
  179. from minport in the range [minport, maxport]."""
  180. exc_info = None
  181. self.config['minport'] = max(1024, self.config['minport'])
  182. self.config['maxport'] = max(self.config['minport'],
  183. self.config['maxport'])
  184. e = (_("maxport less than minport - no ports to check") +
  185. (": %s %s" % (self.config['minport'], self.config['maxport'])))
  186. for port in xrange(self.config['minport'], self.config['maxport'] + 1):
  187. try:
  188. self.singleport_listener.open_port(port, self.config)
  189. if self.config['start_trackerless_client']:
  190. self.dht = UTKhashmir(self.config['bind'],
  191. self.singleport_listener.get_port(),
  192. self.data_dir, self.rawserver,
  193. int(self.config['max_upload_rate'] * 0.01),
  194. rlcount=self.up_ratelimiter.increase_offset,
  195. config=self.config)
  196. break
  197. except socket.error, e:
  198. exc_info = sys.exc_info()
  199. else:
  200. if not listen_fail_ok:
  201. raise BTFailure, (_("Could not open a listening port: %s.") %
  202. str_exc(e) )
  203. self.global_error(logging.CRITICAL,
  204. (_("Could not open a listening port: %s. ") % e) +
  205. (_("Check your port range settings (%s:%s-%s).") %
  206. (self.config['bind'], self.config['minport'],
  207. self.config['maxport'])),
  208. exc_info=exc_info)
  209. def shutdown(self):
  210. df = launch_coroutine(wrap_task(self.rawserver.add_task), self._shutdown)
  211. df.addErrback(lambda f : self.logger.error('shutdown failed!',
  212. exc_info=f.exc_info()))
  213. return df
  214. def _shutdown(self):
  215. self.choker.shutdown()
  216. self.singleport_listener.close_sockets()
  217. for t in self.torrents.itervalues():
  218. try:
  219. df = t.shutdown()
  220. yield df
  221. df.getResult()
  222. totals = t.get_total_transfer()
  223. t.uptotal = t.uptotal_old + totals[0]
  224. t.downtotal = t.downtotal_old + totals[1]
  225. except:
  226. t.logger.debug("Torrent shutdown failed in state: %s", t.state)
  227. print "Torrent shutdown failed in state:", t.state
  228. traceback.print_exc()
  229. # the filepool must be shut down after the torrents,
  230. # or pending ops could never complete
  231. self.filepool_doneflag.set()
  232. if self.resume_from_torrent_config:
  233. self._dump_torrents()
  234. def set_option(self, option, value, infohash=None, dump=True):
  235. if infohash is not None:
  236. t = self.get_torrent(infohash)
  237. t.config[option] = value
  238. if dump:
  239. t._dump_torrent_config()
  240. else:
  241. self.config[option] = value
  242. if dump:
  243. self._dump_global_config()
  244. if option in ['max_upload_rate', 'upload_unit_size']:
  245. self.up_ratelimiter.set_parameters(self.config['max_upload_rate'],
  246. self.config['upload_unit_size'])
  247. elif option == 'max_download_rate':
  248. self.down_ratelimiter.set_parameters(
  249. self.config['max_download_rate'])
  250. #pass # polled from the config automatically by MultiDownload
  251. elif option == 'max_files_open':
  252. self.filepool.set_max_files_open(value)
  253. elif option == 'maxport':
  254. if not self.config['minport'] <= self.singleport_listener.port <= \
  255. self.config['maxport']:
  256. self._find_port()
  257. def add_policy(self, policy):
  258. self.policies.append(policy)
  259. def add_auto_update_policy(self, policy):
  260. self.add_policy(policy)
  261. self.auto_update_policy_index = self.policies.index(policy)
  262. def global_error(self, severity, message, exc_info=None):
  263. self.logger.log(severity, message, exc_info=exc_info)
  264. def create_torrent_non_suck(self, torrent_filename, path_to_data,
  265. hidden=False, feedback=None):
  266. data = open(torrent_filename, 'rb').read()
  267. metainfo = ConvertedMetainfo(bdecode(data))
  268. return self.create_torrent(metainfo, path_to_data, path_to_data,
  269. hidden=hidden, feedback=feedback)
  270. def create_torrent(self, metainfo, save_incomplete_as, save_as,
  271. hidden=False, is_auto_update=False, feedback=None):
  272. if self.is_single_torrent and len(self.torrents) > 0:
  273. raise TooManyTorrents(_("MultiTorrent is set to download only "
  274. "a single torrent, but tried to create more than one."))
  275. infohash = metainfo.infohash
  276. if self.torrent_known(infohash):
  277. if self.torrent_running(infohash):
  278. msg = _("This torrent (or one with the same contents) is "
  279. "already running.")
  280. raise TorrentAlreadyRunning(msg)
  281. else:
  282. raise TorrentAlreadyInQueue(_("This torrent (or one with "
  283. "the same contents) is "
  284. "already waiting to run."))
  285. self._dump_metainfo(metainfo)
  286. #BUG. Use _read_torrent_config for 5.0? --Dave
  287. config = configfile.read_torrent_config(self.config,
  288. self.data_dir,
  289. infohash,
  290. lambda s : self.global_error(logging.ERROR, s))
  291. t = Torrent(metainfo, save_incomplete_as, save_as, self.config,
  292. self.data_dir, self.rawserver, self.choker,
  293. self.singleport_listener, self.up_ratelimiter,
  294. self.down_ratelimiter, self.total_downmeasure,
  295. self.filepool, self.dht, self,
  296. self.log_root, hidden=hidden,
  297. is_auto_update=is_auto_update)
  298. if feedback:
  299. t.add_feedback(feedback)
  300. retdf = Deferred()
  301. def torrent_started(*args):
  302. if config:
  303. t.update_config(config)
  304. t._dump_torrent_config()
  305. if self.resume_from_torrent_config:
  306. self._dump_torrents()
  307. t.metainfo.show_encoding_errors(self.logger.log)
  308. retdf.callback(t)
  309. df = self._init_torrent(t, use_policy=False)
  310. df.addCallback(torrent_started)
  311. return retdf
  312. def remove_torrent(self, ihash, del_files=False):
  313. # this feels redundant. the torrent will stop the download itself,
  314. # can't we accomplish the rest through a callback or something?
  315. if self.torrent_running(ihash):
  316. self.stop_torrent(ihash)
  317. t = self.torrents[ihash]
  318. # super carefully determine whether these are really incomplete files
  319. fs_save_incomplete_in, junk = encode_for_filesystem(
  320. self.config['save_incomplete_in']
  321. )
  322. inco = ((not t.completed) and
  323. (t.working_path != t.destination_path) and
  324. t.working_path.startswith(fs_save_incomplete_in))
  325. del_files = del_files and inco
  326. df = t.shutdown()
  327. df.addCallback(lambda *args: t.remove_state_files(del_files=del_files))
  328. if ihash in self.running:
  329. del self.running[ihash]
  330. # give the torrent a blank feedback, so post-mortem errors don't
  331. # confuse multitorrent
  332. t.feedback = Feedback()
  333. del self.torrents[ihash]
  334. if self.resume_from_torrent_config:
  335. self._dump_torrents()
  336. return df
  337. def reinitialize_torrent(self, infohash):
  338. t = self.get_torrent(infohash)
  339. if self.torrent_running(infohash):
  340. assert t.is_running(), "torrent not running, but in running set"
  341. raise TorrentAlreadyRunning(infohash.encode("hex"))
  342. assert t.state == "failed", "state not failed"
  343. df = self._init_torrent(t, use_policy=False)
  344. return df
  345. def start_torrent(self, infohash):
  346. if self.is_single_torrent and len(self.torrents) > 1:
  347. raise TooManyTorrents(_("MultiTorrent is set to download only "
  348. "a single torrent, but tried to create more than one."))
  349. t = self.get_torrent(infohash)
  350. if self.torrent_running(infohash):
  351. assert t.is_running()
  352. raise TorrentAlreadyRunning(infohash.encode("hex"))
  353. if not t.is_initialized():
  354. raise TorrentNotInitialized(infohash.encode("hex"))
  355. t.logger.debug("starting torrent")
  356. self.running[infohash] = t
  357. t.start_download()
  358. t._dump_torrent_config()
  359. return t.state
  360. def stop_torrent(self, infohash, pause=False):
  361. if not self.torrent_running(infohash):
  362. raise TorrentNotRunning()
  363. t = self.get_torrent(infohash)
  364. assert t.is_running()
  365. t.logger.debug("stopping torrent")
  366. t.stop_download(pause=pause)
  367. del self.running[infohash]
  368. t._dump_torrent_config()
  369. return t.state
  370. def torrent_status(self, infohash, spew=False, fileinfo=False):
  371. torrent = self.get_torrent(infohash)
  372. status = torrent.get_status(spew, fileinfo)
  373. return torrent, status
  374. def get_torrent(self, infohash):
  375. try:
  376. t = self.torrents[infohash]
  377. except KeyError:
  378. raise UnknownInfohash(infohash.encode("hex"))
  379. return t
  380. def get_torrents(self):
  381. return self.torrents.values()
  382. def get_running(self):
  383. return self.running.keys()
  384. def get_visible_torrents(self):
  385. return [t for t in self.torrents.values() if not t.hidden]
  386. def get_visible_running(self):
  387. return [i for i in self.running.keys() if not self.torrents[i].hidden]
  388. def torrent_running(self, ihash):
  389. return ihash in self.running
  390. def torrent_known(self, ihash):
  391. return ihash in self.torrents
  392. def pause(self):
  393. for i in self.running.keys():
  394. self.stop_torrent(i, pause=True)
  395. def unpause(self):
  396. for i in [t.metainfo.infohash for t in self.torrents.values() if t.is_initialized()]:
  397. self.start_torrent(i)
  398. def set_file_priority(self, infohash, filename, priority):
  399. torrent = self.get_torrent(infohash)
  400. if torrent is None or not self.torrent_running(infohash):
  401. return
  402. torrent.set_file_priority(filename, priority)
  403. def set_torrent_priority(self, infohash, priority):
  404. torrent = self.get_torrent(infohash)
  405. if torrent is None:
  406. return
  407. torrent.priority = priority
  408. torrent._dump_torrent_config()
  409. def set_torrent_policy(self, infohash, policy):
  410. torrent = self.get_torrent(infohash)
  411. if torrent is None:
  412. return
  413. torrent.policy = policy
  414. torrent._dump_torrent_config()
  415. def get_all_rates(self):
  416. rates = {}
  417. for infohash, torrent in self.torrents.iteritems():
  418. rates[infohash] = (torrent.get_uprate() or 0,
  419. torrent.get_downrate() or 0)
  420. return rates
  421. def get_variance(self):
  422. return self.bandwidth_manager.current_std, self.bandwidth_manager.max_std
  423. def get_total_rates(self):
  424. u = 0.0
  425. d = 0.0
  426. for torrent in self.torrents.itervalues():
  427. u += torrent.get_uprate() or 0
  428. d += torrent.get_downrate() or 0
  429. return u, d
  430. def get_total_totals(self):
  431. u = 0.0
  432. d = 0.0
  433. for torrent in self.torrents.itervalues():
  434. u += torrent.get_uptotal() or 0
  435. d += torrent.get_downtotal() or 0
  436. return u, d
  437. def auto_update_status(self):
  438. if self.auto_update_policy_index is not None:
  439. aub = self.policies[self.auto_update_policy_index]
  440. return aub.get_auto_update_status()
  441. return None, None, None
  442. def remove_auto_updates_except(self, infohash):
  443. for t in self.torrents.values():
  444. if t.is_auto_update and t.metainfo.infohash != infohash:
  445. self.logger.warning(_("Cleaning up old autoupdate %s") % t.metainfo.name)
  446. self.remove_torrent(t.metainfo.infohash, del_files=True)
  447. ## singletorrent callbacks
  448. def started(self, torrent):
  449. torrent.logger.debug("started torrent")
  450. assert torrent.infohash in self.torrents
  451. torrent._dump_torrent_config()
  452. for policy in self.policies:
  453. policy.started(torrent)
  454. def failed(self, torrent):
  455. torrent.logger.debug("torrent failed")
  456. if torrent.infohash not in self.running:
  457. return
  458. del self.running[torrent.infohash]
  459. t = self.get_torrent(torrent.infohash)
  460. for policy in self.policies:
  461. policy.failed(t)
  462. def finishing(self, torrent):
  463. torrent.logger.debug("torrent finishing")
  464. t = self.get_torrent(torrent.infohash)
  465. def finished(self, torrent):
  466. # set done-flag
  467. self.isDone = True
  468. #
  469. torrent.logger.debug("torrent finished")
  470. t = self.get_torrent(torrent.infohash)
  471. t._dump_torrent_config()
  472. for policy in self.policies:
  473. policy.finished(t)
  474. def exception(self, torrent, text):
  475. torrent.logger.debug("torrent threw exception: " + text)
  476. if torrent.infohash not in self.torrents:
  477. return
  478. for policy in self.policies:
  479. policy.exception(torrent, text)
  480. def error(self, torrent, level, text):
  481. torrent.logger.log(level, text)
  482. if torrent.infohash not in self.torrents:
  483. return
  484. for policy in self.policies:
  485. policy.error(torrent, level, text)
  486. ### persistence
  487. ## These should be the .torrent file!
  488. #################
  489. def _dump_metainfo(self, metainfo):
  490. infohash = metainfo.infohash
  491. path = os.path.join(self.data_dir, 'metainfo',
  492. infohash.encode('hex'))
  493. f = file(path+'.new', 'wb')
  494. f.write(metainfo.to_data())
  495. f.close()
  496. shutil.move(path+'.new', path)
  497. def _read_metainfo(self, infohash):
  498. path = os.path.join(self.data_dir, 'metainfo',
  499. infohash.encode('hex'))
  500. f = file(path, 'rb')
  501. data = f.read()
  502. f.close()
  503. return ConvertedMetainfo(bdecode(data))
  504. #################
  505. def _read_torrent_config(self, infohash):
  506. path = os.path.join(self.data_dir, 'torrents', infohash.encode('hex'))
  507. if not os.path.exists(path):
  508. raise BTFailure,_("Coult not open the torrent config: " + infohash.encode('hex'))
  509. f = file(path, 'rb')
  510. data = f.read()
  511. f.close()
  512. try:
  513. torrent_config = cPickle.loads(data)
  514. except:
  515. # backward compatibility with <= 4.9.3
  516. torrent_config = bdecode(data)
  517. for k, v in torrent_config.iteritems():
  518. try:
  519. torrent_config[k] = v.decode('utf8')
  520. if k in ('destination_path', 'working_path'):
  521. torrent_config[k] = encode_for_filesystem(torrent_config[k])[0]
  522. except:
  523. pass
  524. if not torrent_config.get('destination_path'):
  525. raise BTFailure( _("Invalid torrent config file"))
  526. if not torrent_config.get('working_path'):
  527. raise BTFailure( _("Invalid torrent config file"))
  528. if get_filesystem_encoding() == None:
  529. # These paths should both be unicode. If they aren't, they are the
  530. # broken product of some old version, and probably are in the
  531. # encoding we used to use in config files. Attempt to recover.
  532. dp = torrent_config['destination_path']
  533. if isinstance(dp, str):
  534. try:
  535. dp = dp.decode(old_broken_config_subencoding)
  536. torrent_config['destination_path'] = dp
  537. except:
  538. raise BTFailure( _("Invalid torrent config file"))
  539. wp = torrent_config['working_path']
  540. if isinstance(wp, str):
  541. try:
  542. wp = wp.decode(old_broken_config_subencoding)
  543. torrent_config['working_path'] = wp
  544. except:
  545. raise BTFailure( _("Invalid torrent config file"))
  546. return torrent_config
  547. def _dump_global_config(self):
  548. # BUG: we can save to different sections later
  549. section = 'bittorrent'
  550. configfile.save_global_config(self.config, section,
  551. lambda *e : self.logger.error(*e))
  552. def _dump_torrents(self):
  553. assert self.resume_from_torrent_config
  554. self.last_save_time = bttime()
  555. r = []
  556. def write_entry(infohash, t):
  557. r.append(' '.join((infohash.encode('hex'),
  558. str(t.uptotal), str(t.downtotal))))
  559. r.append('BitTorrent UI state file, version 5')
  560. r.append('Queued torrents')
  561. for t in self.torrents.values():
  562. write_entry(t.metainfo.infohash, self.torrents[t.metainfo.infohash])
  563. r.append('End')
  564. f = None
  565. try:
  566. path = os.path.join(self.data_dir, 'ui_state')
  567. f = file(path+'.new', 'wb')
  568. f.write('\n'.join(r) + '\n')
  569. f.close()
  570. shutil.move(path+'.new', path)
  571. except Exception, e:
  572. self.logger.error(_("Could not save UI state: ") + str_exc(e))
  573. if f is not None:
  574. f.close()
  575. def _init_torrent(self, t, initialize=True, use_policy=True):
  576. self.torrents[t.infohash] = t
  577. if not initialize:
  578. t.logger.debug("created torrent")
  579. return
  580. t.logger.debug("created torrent, initializing")
  581. df = t.initialize()
  582. if use_policy and t.policy == "start":
  583. df.addCallback(lambda r, t: self.start_torrent(t.infohash), t)
  584. return df
  585. def initialize_torrents(self):
  586. df = launch_coroutine(wrap_task(self.rawserver.add_task), self._initialize_torrents)
  587. df.addErrback(lambda f : self.logger.error('initialize_torrents failed!',
  588. exc_info=f.exc_info()))
  589. return df
  590. def _initialize_torrents(self):
  591. self.logger.debug("initializing torrents")
  592. for t in copy(self.torrents).itervalues():
  593. if t in self.torrents.values() and t.state == "created":
  594. df = self._init_torrent(t)
  595. # HACK
  596. #yield df
  597. #df.getResult()
  598. # this function is so nasty!
  599. def _restore_state(self, init_torrents):
  600. def decode_line(line):
  601. hashtext = line[:40]
  602. try:
  603. infohash = InfoHashType(hashtext.decode('hex'))
  604. except:
  605. raise BTFailure(_("Invalid state file contents"))
  606. if len(infohash) != 20:
  607. raise BTFailure(_("Invalid state file contents"))
  608. if infohash in self.torrents:
  609. raise BTFailure(_("Invalid state file (duplicate entry)"))
  610. try:
  611. metainfo = self._read_metainfo(infohash)
  612. except OSError, e:
  613. try:
  614. f.close()
  615. except:
  616. pass
  617. self.logger.error((_("Error reading metainfo file \"%s\".") %
  618. hashtext) + " (" + str_exc(e)+ "), " +
  619. _("cannot restore state completely"))
  620. return None
  621. except Exception, e:
  622. self.logger.error((_("Corrupt data in metainfo \"%s\", cannot restore torrent.") % hashtext) +
  623. '('+str_exc(e)+')')
  624. return None
  625. b = encode_for_filesystem(u'')[0]
  626. t = Torrent(metainfo, b, b, self.config, self.data_dir,
  627. self.rawserver, self.choker,
  628. self.singleport_listener, self.up_ratelimiter,
  629. self.down_ratelimiter,
  630. self.total_downmeasure, self.filepool, self.dht, self,
  631. self.log_root)
  632. t.metainfo.reported_errors = True # suppress redisplay on restart
  633. if infohash != t.metainfo.infohash:
  634. self.logger.error((_("Corrupt data in \"%s\", cannot restore torrent.") % hashtext) +
  635. _("(infohash mismatch)"))
  636. return None
  637. if len(line) == 41:
  638. t.working_path = None
  639. t.destination_path = None
  640. return infohash, t
  641. try:
  642. if version < 2:
  643. t.working_path = line[41:-1].decode('string_escape')
  644. t.working_path = t.working_path.decode('utf-8')
  645. t.working_path = encode_for_filesystem(t.working_path)[0]
  646. t.destination_path = t.working_path
  647. elif version == 3:
  648. up, down, working_path = line[41:-1].split(' ', 2)
  649. t.uptotal = t.uptotal_old = int(up)
  650. t.downtotal = t.downtotal_old = int(down)
  651. t.working_path = working_path.decode('string_escape')
  652. t.working_path = t.working_path.decode('utf-8')
  653. t.working_path = encode_for_filesystem(t.working_path)[0]
  654. t.destination_path = t.working_path
  655. elif version >= 4:
  656. up, down = line[41:-1].split(' ', 1)
  657. t.uptotal = t.uptotal_old = int(up)
  658. t.downtotal = t.downtotal_old = int(down)
  659. except ValueError: # unpack, int(), decode()
  660. raise BTFailure(_("Invalid state file (bad entry)"))
  661. torrent_config = self.config
  662. try:
  663. if version < 5:
  664. torrent_config = configfile.read_torrent_config(
  665. self.config,
  666. self.data_dir,
  667. infohash,
  668. lambda s : self.global_error(logging.ERROR, s))
  669. else:
  670. torrent_config = self._read_torrent_config(infohash)
  671. t.update_config(torrent_config)
  672. except BTFailure, e:
  673. self.logger.error("Read torrent config failed",
  674. exc_info=sys.exc_info())
  675. # if read_torrent_config fails then ignore the torrent...
  676. return None
  677. return infohash, t
  678. # BEGIN _restore_state
  679. assert self.resume_from_torrent_config
  680. filename = os.path.join(self.data_dir, 'ui_state')
  681. if not os.path.exists(filename):
  682. return
  683. f = None
  684. try:
  685. f = file(filename, 'rb')
  686. lines = f.readlines()
  687. f.close()
  688. except Exception, e:
  689. if f is not None:
  690. f.close()
  691. raise BTFailure(str_exc(e))
  692. i = iter(lines)
  693. try:
  694. txt = 'BitTorrent UI state file, version '
  695. version = i.next()
  696. if not version.startswith(txt):
  697. raise BTFailure(_("Bad UI state file"))
  698. try:
  699. version = int(version[len(txt):-1])
  700. except:
  701. raise BTFailure(_("Bad UI state file version"))
  702. if version > 5:
  703. raise BTFailure(_("Unsupported UI state file version (from "
  704. "newer client version?)"))
  705. if version < 3:
  706. if i.next() != 'Running/queued torrents\n':
  707. raise BTFailure(_("Invalid state file contents"))
  708. else:
  709. if i.next() != 'Running torrents\n' and version != 5:
  710. raise BTFailure(_("Invalid state file contents"))
  711. while version < 5:
  712. line = i.next()
  713. if line == 'Queued torrents\n':
  714. break
  715. t = decode_line(line)
  716. if t is None:
  717. continue
  718. infohash, t = t
  719. df = self._init_torrent(t, initialize=init_torrents)
  720. while True:
  721. line = i.next()
  722. if (version < 5 and line == 'Known torrents\n') or (version == 5 and line == 'End\n'):
  723. break
  724. t = decode_line(line)
  725. if t is None:
  726. continue
  727. infohash, t = t
  728. if t.destination_path is None:
  729. raise BTFailure(_("Invalid state file contents"))
  730. df = self._init_torrent(t, initialize=init_torrents)
  731. while version < 5:
  732. line = i.next()
  733. if line == 'End\n':
  734. break
  735. t = decode_line(line)
  736. if t is None:
  737. continue
  738. infohash, t = t
  739. df = self._init_torrent(t, initialize=init_torrents)
  740. except StopIteration:
  741. raise BTFailure(_("Invalid state file contents"))