UI.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702
  1. # The contents of this file are subject to the BitTorrent Open Source License
  2. # Version 1.1 (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. # written by Matt Chisholm
  11. from __future__ import division
  12. import os
  13. import os.path
  14. import atexit
  15. import itertools
  16. import webbrowser
  17. from copy import copy
  18. import logging
  19. import logging.handlers
  20. from BTL.translation import _
  21. import BTL.stackthreading as threading
  22. from BTL.platform import bttime, efs2
  23. from BTL.obsoletepythonsupport import set
  24. from BTL.yielddefer import launch_coroutine
  25. from BTL.defer import ThreadedDeferred, wrap_task
  26. from BTL.ThreadProxy import ThreadProxy
  27. from BTL.exceptions import str_exc
  28. from BTL.formatters import percentify, Size, Rate, Duration
  29. from BitTorrent import GetTorrent
  30. from BitTorrent import LaunchPath
  31. from BitTorrent.MultiTorrent import UnknownInfohash, TorrentAlreadyInQueue, TorrentAlreadyRunning, TorrentNotRunning
  32. from BitTorrent.platform import desktop
  33. from BitTorrent.Torrent import *
  34. state_dict = {("created", "stop", False): _("Paused"),
  35. ("created", "stop", True): _("Paused"),
  36. ("created", "start", False): _("Starting"),
  37. ("created", "start", True): _("Starting"),
  38. ("created", "auto", False): _("Starting"),
  39. ("created", "auto", True): _("Starting"),
  40. ("initializing", "stop", False): _("Paused"),
  41. ("initializing", "stop", True): _("Paused"),
  42. ("initializing", "start", False): _("Starting"),
  43. ("initializing", "start", True): _("Starting"),
  44. ("initializing", "auto", False): _("Starting"),
  45. ("initializing", "auto", True): _("Starting"),
  46. ("initialized", "stop", False): _("Paused"),
  47. ("initialized", "stop", True): _("Paused"),
  48. ("initialized", "start", False): _("Starting"),
  49. ("initialized", "start", True): _("Starting"),
  50. ("initialized", "auto", False): _("Queued"),
  51. ("initialized", "auto", True): _("Complete"),
  52. ("running", "stop", False): _("Downloading"),
  53. ("running", "stop", True): _("Seeding"),
  54. ("running", "start", False): _("Downloading"),
  55. ("running", "start", True): _("Seeding"),
  56. ("running", "auto", False): _("Downloading"),
  57. ("running", "auto", True): _("Complete"),
  58. ("finishing", "stop", False): _("Finishing"),
  59. ("finishing", "stop", True): _("Finishing"),
  60. ("finishing", "start", False): _("Finishing"),
  61. ("finishing", "start", True): _("Finishing"),
  62. ("finishing", "auto", False): _("Finishing"),
  63. ("finishing", "auto", True): _("Finishing"),
  64. ("failed", "stop", False): _("Error"),
  65. ("failed", "stop", True): _("Error"),
  66. ("failed", "start", False): _("Error"),
  67. ("failed", "start", True): _("Error"),
  68. ("failed", "auto", False): _("Error"),
  69. ("failed", "auto", True): _("Error"),}
  70. def ip_sort(a_str,b_str):
  71. """Fast IP address sorting function"""
  72. for a,b in itertools.izip(a_str.split('.'), b_str.split('.')):
  73. if a == b:
  74. continue
  75. if len(a) == len(b):
  76. return cmp(a,b)
  77. return cmp(int(a), int(b))
  78. return 0
  79. def find_dir(path):
  80. if os.path.isdir(path):
  81. return path
  82. directory, garbage = os.path.split(path)
  83. while directory:
  84. if os.access(directory, os.F_OK) and os.access(directory, os.W_OK):
  85. return directory
  86. directory, garbage = os.path.split(directory)
  87. if garbage == '':
  88. break
  89. return None
  90. def smart_dir(path):
  91. path = find_dir(path)
  92. if path is None:
  93. path = desktop
  94. return path
  95. if os.name == 'nt':
  96. disk_term = _("drive")
  97. elif os.name == 'posix' and os.uname()[0] == 'Darwin':
  98. disk_term = _("volume")
  99. else:
  100. disk_term = _("disk")
  101. class BasicTorrentObject(object):
  102. """Object for holding all information about a torrent"""
  103. def __init__(self, torrent):
  104. self.torrent = torrent
  105. self.pending = None
  106. self.infohash = torrent.metainfo.infohash
  107. self.metainfo = torrent.metainfo
  108. self.destination_path = torrent.destination_path
  109. self.working_path = torrent.working_path
  110. self.state = torrent.state
  111. self.policy = torrent.policy
  112. self.completed = torrent.completed
  113. self.priority = torrent.priority
  114. self.completion = None
  115. self.piece_states = None
  116. self.uptotal = 0
  117. self.downtotal = 0
  118. self.up_down_ratio = 0
  119. self.peers = 0
  120. self.dead = False
  121. self.statistics = {}
  122. self.handler = logging.handlers.MemoryHandler(0) # capacity is ignored
  123. logging.getLogger("core.MultiTorrent." + repr(self.infohash)).addHandler(self.handler)
  124. def update(self, torrent, statistics):
  125. self.torrent = torrent
  126. self.statistics = statistics
  127. self.destination_path = torrent.destination_path
  128. self.working_path = torrent.working_path
  129. self.state = torrent.state
  130. self.policy = torrent.policy
  131. self.completed = torrent.completed
  132. self.priority = statistics['priority']
  133. self.completion = statistics['fractionDone']
  134. self.piece_states = statistics['pieceStates']
  135. self.uptotal += statistics.get('upTotal' , 0)
  136. self.downtotal += statistics.get('downTotal', 0)
  137. try:
  138. self.up_down_ratio = self.uptotal / self.torrent.metainfo.total_bytes
  139. except ZeroDivisionError:
  140. self.up_down_ratio = 0
  141. self.peers = statistics.get('numPeers', 0)
  142. def wants_peers(self):
  143. return True
  144. def wants_files(self):
  145. return self.metainfo.is_batch
  146. def clean_up(self):
  147. if self.dead:
  148. return
  149. self.dead = True
  150. del self.torrent
  151. del self.metainfo
  152. logging.getLogger("core.MultiTorrent." + repr(self.infohash)).removeHandler(self.handler)
  153. class BasicApp(object):
  154. torrent_object_class = BasicTorrentObject
  155. def __init__(self, config):
  156. self.started = 0
  157. self.multitorrent = None
  158. self.config = config
  159. self.torrents = {}
  160. self.external_torrents = []
  161. self.installer_to_launch_at_exit = None
  162. self.logger = logging.getLogger('UI')
  163. self.logger.setLevel(logging.INFO)
  164. self.next_autoupdate_nag = bttime()
  165. def gui_wrap(_f, *args, **kwargs):
  166. f(*args, **kwargs)
  167. self.gui_wrap = gui_wrap
  168. self.open_external_torrents_deferred = None
  169. def quit(self):
  170. if self.doneflag:
  171. self.doneflag.set()
  172. def visit_url(self, url, callback=None):
  173. """Visit a URL in the user's browser"""
  174. t = threading.Thread(target=self._visit_url, args=(url, callback))
  175. t.start()
  176. def _visit_url(self, url, callback=None):
  177. """Visit a URL in the user's browser non-blockingly"""
  178. webbrowser.open(url)
  179. if callback:
  180. self.gui_wrap(callback)
  181. def open_torrent_arg(self, path):
  182. """Open a torrent from path (URL, file) non-blockingly"""
  183. df = ThreadedDeferred(self.gui_wrap, GetTorrent.get_quietly, path)
  184. return df
  185. def publish_torrent(self, torrent, publish_path):
  186. df = self.open_torrent_arg(torrent)
  187. yield df
  188. try:
  189. metainfo = df.getResult()
  190. except GetTorrent.GetTorrentException:
  191. self.logger.exception("publish_torrent failed")
  192. return
  193. df = self.multitorrent.create_torrent(metainfo, efs2(publish_path), efs2(publish_path))
  194. yield df
  195. df.getResult()
  196. def open_torrent_arg_with_callbacks(self, path):
  197. """Open a torrent from path (URL, file) non-blockingly, and
  198. call the appropriate GUI callback when necessary."""
  199. def errback(f):
  200. exc_type, value, tb = f.exc_info()
  201. if issubclass(exc_type, GetTorrent.GetTorrentException):
  202. self.logger.critical(str_exc(value))
  203. else:
  204. self.logger.error("open_torrent_arg_with_callbacks failed",
  205. exc_info=f.exc_info())
  206. def callback(metainfo):
  207. def open(metainfo):
  208. df = self.multitorrent.torrent_known(metainfo.infohash)
  209. yield df
  210. known = df.getResult()
  211. if known:
  212. self.torrent_already_open(metainfo)
  213. else:
  214. df = self.open_torrent_metainfo(metainfo)
  215. if df is not None:
  216. yield df
  217. try:
  218. df.getResult()
  219. except TorrentAlreadyInQueue:
  220. pass
  221. except TorrentAlreadyRunning:
  222. pass
  223. launch_coroutine(self.gui_wrap, open, metainfo)
  224. df = self.open_torrent_arg(path)
  225. df.addCallback(callback)
  226. df.addErrback(errback)
  227. return df
  228. def append_external_torrents(self, *a):
  229. """Append external torrents (such as those specified on the
  230. command line) so that they can be processed (for save paths,
  231. error reporting, etc.) once the GUI has started up."""
  232. self.external_torrents.extend(a)
  233. def _open_external_torrents(self):
  234. """Open torrents added externally (on the command line before
  235. startup) in a non-blocking yet serial way."""
  236. while self.external_torrents:
  237. arg = self.external_torrents.pop(0)
  238. df = self.open_torrent_arg(arg)
  239. yield df
  240. try:
  241. metainfo = df.getResult()
  242. except GetTorrent.GetTorrentException:
  243. self.logger.exception("Failed to get torrent")
  244. continue
  245. if metainfo is not None:
  246. # metainfo may be none if IE passes us a path to a
  247. # file in its cache that has already been deleted
  248. # because it came from a website which set
  249. # Pragma:No-Cache on it.
  250. # See GetTorrent.get_quietly().
  251. df = self.multitorrent.torrent_known(metainfo.infohash)
  252. yield df
  253. known = df.getResult()
  254. if known:
  255. self.torrent_already_open(metainfo)
  256. else:
  257. df = self.open_torrent_metainfo(metainfo)
  258. if df is not None:
  259. yield df
  260. try:
  261. df.getResult()
  262. except TorrentAlreadyInQueue:
  263. pass
  264. except TorrentAlreadyRunning:
  265. pass
  266. self.open_external_torrents_deferred = None
  267. def open_external_torrents(self):
  268. """Open torrents added externally (on the command line before startup)."""
  269. if self.open_external_torrents_deferred is None and \
  270. len(self.external_torrents):
  271. self.open_external_torrents_deferred = launch_coroutine(self.gui_wrap, self._open_external_torrents)
  272. def callback(*a):
  273. self.open_external_torrents_deferred = None
  274. def errback(f):
  275. callback()
  276. self.logger.error("open_external_torrents failed:",
  277. exc_info=f.exc_info())
  278. self.open_external_torrents_deferred.addCallback(callback)
  279. self.open_external_torrents_deferred.addErrback(errback)
  280. def torrent_already_open(self, metainfo):
  281. """Tell the user."""
  282. raise NotImplementedError('BasicApp.torrent_already_open() not implemented')
  283. def open_torrent_metainfo(self, metainfo):
  284. """Get a valid save path from the user, and then tell
  285. multitorrent to create a new torrent from metainfo."""
  286. raise NotImplementedError('BasicApp.open_torrent_metainfo() not implemented')
  287. def launch_torrent(self, infohash):
  288. """Launch the torrent contents according to operating system."""
  289. if infohash in self.torrents:
  290. torrent = self.torrents[infohash]
  291. if torrent.metainfo.is_batch:
  292. LaunchPath.launchdir(torrent.working_path)
  293. else:
  294. LaunchPath.launchfile(torrent.working_path)
  295. def launch_torrent_folder(self, infohash):
  296. """Launch the torrent location according to operating system."""
  297. if infohash in self.torrents:
  298. torrent = self.torrents[infohash]
  299. if torrent.metainfo.is_batch:
  300. LaunchPath.launchdir(torrent.working_path)
  301. else:
  302. path, file = os.path.split(torrent.working_path)
  303. LaunchPath.launchdir(path)
  304. def launch_installer_at_exit(self):
  305. LaunchPath.launchfile(self.installer_to_launch_at_exit)
  306. def do_log(self, severity, text):
  307. raise NotImplementedError('BasicApp.do_log() not implemented')
  308. def attach_multitorrent(self, multitorrent, doneflag):
  309. self.multitorrent = multitorrent
  310. self.multitorrent_doneflag = doneflag
  311. self.rawserver = multitorrent.obj.rawserver
  312. self.multitorrent.initialize_torrents()
  313. def init_updates(self):
  314. """Make status request at regular intervals."""
  315. raise NotImplementedError('BasicApp.init_updates() not implemented')
  316. def make_statusrequest(self, event = None):
  317. """Make status request."""
  318. df = launch_coroutine(self.gui_wrap, self.update_status)
  319. def errback(f):
  320. self.logger.error("update_status failed",
  321. exc_info=f.exc_info())
  322. df.addErrback(errback)
  323. return True
  324. def _thread_proxy(self, obj):
  325. return ThreadProxy(obj,
  326. self.gui_wrap,
  327. wrap_task(self.rawserver.external_add_task))
  328. def update_single_torrent(self, infohash):
  329. torrent = self.torrents[infohash]
  330. df = self.multitorrent.torrent_status(infohash,
  331. torrent.wants_peers(),
  332. torrent.wants_files()
  333. )
  334. yield df
  335. try:
  336. core_torrent, statistics = df.getResult()
  337. except UnknownInfohash:
  338. # looks like it's gone now
  339. if infohash in self.torrents:
  340. self._do_remove_torrent(infohash)
  341. else:
  342. # the infohash might have been removed from torrents
  343. # while we were yielding above, so we need to check
  344. if infohash in self.torrents:
  345. core_torrent = self._thread_proxy(core_torrent)
  346. torrent.update(core_torrent, statistics)
  347. self.update_torrent(torrent)
  348. def update_status(self):
  349. """Update torrent information based on the results of making a
  350. status request."""
  351. df = self.multitorrent.get_torrents()
  352. yield df
  353. torrents = df.getResult()
  354. infohashes = set()
  355. au_torrents = {}
  356. for torrent in torrents:
  357. torrent = self._thread_proxy(torrent)
  358. infohashes.add(torrent.metainfo.infohash)
  359. if torrent.metainfo.infohash not in self.torrents:
  360. if self.config.get('show_hidden_torrents') or not torrent.hidden:
  361. # create new torrent widget
  362. to = self.new_displayed_torrent(torrent)
  363. if torrent.is_auto_update:
  364. au_torrents[torrent.metainfo.infohash] = torrent
  365. for infohash, torrent in copy(self.torrents).iteritems():
  366. # remove nonexistent torrents
  367. if infohash not in infohashes:
  368. self._do_remove_torrent(infohash)
  369. total_completion = 0
  370. total_bytes = 0
  371. for infohash, torrent in copy(self.torrents).iteritems():
  372. # update existing torrents
  373. df = self.multitorrent.torrent_status(infohash,
  374. torrent.wants_peers(),
  375. torrent.wants_files()
  376. )
  377. yield df
  378. try:
  379. core_torrent, statistics = df.getResult()
  380. except UnknownInfohash:
  381. # looks like it's gone now
  382. if infohash in self.torrents:
  383. self._do_remove_torrent(infohash)
  384. else:
  385. # the infohash might have been removed from torrents
  386. # while we were yielding above, so we need to check
  387. if infohash in self.torrents:
  388. core_torrent = self._thread_proxy(core_torrent)
  389. torrent.update(core_torrent, statistics)
  390. self.update_torrent(torrent)
  391. if statistics['fractionDone'] is not None:
  392. amount_done = statistics['fractionDone'] * torrent.metainfo.total_bytes
  393. total_completion += amount_done
  394. total_bytes += torrent.metainfo.total_bytes
  395. all_completed = False
  396. if total_bytes == 0:
  397. average_completion = 0
  398. else:
  399. average_completion = total_completion / total_bytes
  400. if total_completion == total_bytes:
  401. all_completed = True
  402. df = self.multitorrent.auto_update_status()
  403. yield df
  404. available_version, installable_version, delay = df.getResult()
  405. if available_version is not None:
  406. if installable_version is None:
  407. self.notify_of_new_version(available_version)
  408. else:
  409. if self.installer_to_launch_at_exit is None:
  410. atexit.register(self.launch_installer_at_exit)
  411. if installable_version not in au_torrents:
  412. df = self.multitorrent.get_torrent(installable_version)
  413. yield df
  414. torrent = df.getResult()
  415. torrent = ThreadProxy(torrent, self.gui_wrap)
  416. else:
  417. torrent = au_torrents[installable_version]
  418. self.installer_to_launch_at_exit = torrent.working_path
  419. if bttime() > self.next_autoupdate_nag:
  420. self.prompt_for_quit_for_new_version(available_version)
  421. self.next_autoupdate_nag = bttime() + delay
  422. def get_global_stats(mt):
  423. stats = {}
  424. u, d = mt.get_total_rates()
  425. stats['total_uprate'] = Rate(u)
  426. stats['total_downrate'] = Rate(d)
  427. u, d = mt.get_total_totals()
  428. stats['total_uptotal'] = Size(u)
  429. stats['total_downtotal'] = Size(d)
  430. torrents = mt.get_visible_torrents()
  431. running = mt.get_visible_running()
  432. stats['num_torrents'] = len(torrents)
  433. stats['num_running_torrents'] = len(running)
  434. stats['num_connections'] = 0
  435. for t in torrents:
  436. stats['num_connections'] += t.get_num_connections()
  437. try:
  438. stats['avg_connections'] = (stats['num_connections'] /
  439. stats['num_running_torrents'])
  440. except ZeroDivisionError:
  441. stats['avg_connections'] = 0
  442. stats['avg_connections'] = "%.02f" % stats['avg_connections']
  443. return stats
  444. df = self.multitorrent.call_with_obj(get_global_stats)
  445. yield df
  446. global_stats = df.getResult()
  447. yield average_completion, all_completed, global_stats
  448. def _update_status(self, total_completion):
  449. raise NotImplementedError('BasicApp._update_status() not implemented')
  450. def new_displayed_torrent(self, torrent):
  451. """Tell the UI that it should draw a new torrent."""
  452. torrent_object = self.torrent_object_class(torrent)
  453. self.torrents[torrent.metainfo.infohash] = torrent_object
  454. return torrent_object
  455. def torrent_removed(self, infohash):
  456. """Tell the GUI that a torrent has been removed, by it, or by
  457. multitorrent."""
  458. raise NotImplementedError('BasicApp.torrent_removed() removing missing torrents not implemented')
  459. def update_torrent(self, torrent_object):
  460. """Tell the GUI to update a torrent's info."""
  461. raise NotImplementedError('BasicApp.update_torrent() updating existing torrents not implemented')
  462. def notify_of_new_version(self, version):
  463. print 'got auto_update_status', version
  464. pass
  465. def prompt_for_quit_for_new_version(self, version):
  466. print 'got new version', version
  467. pass
  468. # methods that are used to send commands to MultiTorrent
  469. def send_config(self, option, value, infohash=None):
  470. """Tell multitorrent to set a config item."""
  471. self.config[option] = value
  472. if self.multitorrent:
  473. self.multitorrent.set_option(option, value, infohash)
  474. def remove_infohash(self, infohash, del_files=False):
  475. """Tell multitorrent to remove a torrent."""
  476. df = self.multitorrent.remove_torrent(infohash, del_files=del_files)
  477. yield df
  478. try:
  479. df.getResult()
  480. except KeyError:
  481. pass # it was already gone, who cares
  482. if infohash in self.torrents:
  483. self._do_remove_torrent(infohash)
  484. def _do_remove_torrent(self, infohash):
  485. self.torrent_removed(infohash)
  486. torrent_object = self.torrents.pop(infohash)
  487. torrent_object.clean_up()
  488. def set_file_priority(self, infohash, filenames, dowhat):
  489. """Tell multitorrent to set file priorities."""
  490. for f in filenames:
  491. self.multitorrent.set_file_priority(infohash, f, dowhat)
  492. def stop_torrent(self, infohash, pause=False):
  493. """Tell multitorrent to stop a torrent."""
  494. torrent = self.torrents[infohash]
  495. if (torrent and torrent.pending == None):
  496. torrent.pending = "stop"
  497. df = self.multitorrent.set_torrent_policy(infohash, "stop")
  498. yield df
  499. try:
  500. df.getResult()
  501. except TorrentNotRunning:
  502. pass
  503. if torrent.state == "running":
  504. df = self.multitorrent.stop_torrent(infohash, pause=pause)
  505. yield df
  506. torrent.state = df.getResult()
  507. torrent.pending = None
  508. yield True
  509. def start_torrent(self, infohash):
  510. """Tell multitorrent to start a torrent."""
  511. torrent = self.torrents[infohash]
  512. if (torrent and torrent.pending == None and
  513. torrent.state in ["failed", "initialized"]):
  514. torrent.pending = "start"
  515. if torrent.state == "failed":
  516. df = self.multitorrent.reinitialize_torrent(infohash)
  517. yield df
  518. df.getResult()
  519. df = self.multitorrent.set_torrent_policy(infohash, "auto")
  520. yield df
  521. df.getResult()
  522. torrent.pending = None
  523. yield True
  524. def force_start_torrent(self, infohash):
  525. torrent = self.torrents[infohash]
  526. if (torrent and torrent.pending == None):
  527. torrent.pending = "force start"
  528. df = self.multitorrent.set_torrent_policy(infohash, "start")
  529. yield df
  530. df.getResult()
  531. if torrent.state in ["failed", "initialized"]:
  532. if torrent.state == "failed":
  533. df = self.multitorrent.reinitialize_torrent(infohash)
  534. yield df
  535. df.getResult()
  536. df = self.multitorrent.start_torrent(infohash)
  537. yield df
  538. try:
  539. torrent.state = df.getResult()
  540. except TorrentAlreadyRunning:
  541. torrent.state = "running"
  542. torrent.pending = None
  543. yield True
  544. def no_op(self):
  545. pass
  546. def external_command(self, action, *datas):
  547. """For communication via IPC"""
  548. datas = [ d.decode('utf-8') for d in datas ]
  549. if action == 'start_torrent':
  550. assert len(datas) == 1, 'incorrect data length'
  551. self.append_external_torrents(*datas)
  552. self.logger.info('got external_command:start_torrent: "%s"' % datas[0])
  553. # this call does Ye Olde Threadede Deferrede:
  554. self.open_external_torrents()
  555. elif action == 'publish_torrent':
  556. self.logger.info('got external_command:publish_torrent: "%s" as "%s"' % datas)
  557. launch_coroutine(self.gui_wrap, self.publish_torrent, datas[0], datas[1])
  558. elif action == 'show_error':
  559. assert len(datas) == 1, 'incorrect data length'
  560. self.logger.error(datas[0])
  561. elif action == 'no-op':
  562. self.no_op()
  563. self.logger.info('got external_command: no-op')
  564. else:
  565. self.logger.warning('got unknown external_command: %s' % str(action))
  566. # fun.
  567. #code = action + ' '.join(datas)
  568. #self.logger.warning('eval: %s' % code)
  569. #exec code