]> git.lizzy.rs Git - torbrowser-launcher.git/blob - torbrowser_launcher/launcher.py
Update location of start-tor-browser for TBB 4.5, and remove accept_links feature...
[torbrowser-launcher.git] / torbrowser_launcher / launcher.py
1 """
2 Tor Browser Launcher
3 https://github.com/micahflee/torbrowser-launcher/
4
5 Copyright (c) 2013-2014 Micah Lee <micah@micahflee.com>
6
7 Permission is hereby granted, free of charge, to any person
8 obtaining a copy of this software and associated documentation
9 files (the "Software"), to deal in the Software without
10 restriction, including without limitation the rights to use,
11 copy, modify, merge, publish, distribute, sublicense, and/or sell
12 copies of the Software, and to permit persons to whom the
13 Software is furnished to do so, subject to the following
14 conditions:
15
16 The above copyright notice and this permission notice shall be
17 included in all copies or substantial portions of the Software.
18
19 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
21 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
23 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
24 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26 OTHER DEALINGS IN THE SOFTWARE.
27 """
28
29 import os, subprocess, time, json, tarfile, hashlib, lzma, threading, re
30 from twisted.internet import reactor
31 from twisted.web.client import Agent, RedirectAgent, ResponseDone, ResponseFailed
32 from twisted.web.http_headers import Headers
33 from twisted.internet.protocol import Protocol
34 from twisted.internet.ssl import ClientContextFactory
35 from twisted.internet.error import DNSLookupError
36
37 import OpenSSL
38
39 import pygtk
40 pygtk.require('2.0')
41 import gtk
42
43 class TryStableException(Exception):
44     pass
45
46 class TryDefaultMirrorException(Exception):
47     pass
48
49 class DownloadErrorException(Exception):
50     pass
51
52 class VerifyTorProjectCert(ClientContextFactory):
53     def __init__(self, torproject_pem):
54         self.torproject_ca = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, open(torproject_pem, 'r').read())
55
56     def getContext(self, host, port):
57         ctx = ClientContextFactory.getContext(self)
58         ctx.set_verify_depth(0)
59         ctx.set_verify(OpenSSL.SSL.VERIFY_PEER | OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname)
60         return ctx
61
62     def verifyHostname(self, connection, cert, errno, depth, preverifyOK):
63         return cert.digest('sha256') == self.torproject_ca.digest('sha256')
64
65 class Launcher:
66     def __init__(self, common, url_list):
67         print _('Starting launcher dialog')
68         self.common = common
69         self.url_list = url_list
70
71         # init launcher
72         self.set_gui(None, '', [])
73         self.launch_gui = True
74         print "LATEST VERSION", self.common.settings['latest_version']
75         self.common.build_paths(self.common.settings['latest_version'])
76
77         if self.common.settings['update_over_tor']:
78             try:
79                 import txsocksx
80             except ImportError:
81                 md = gtk.MessageDialog(None, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, _("The python-txsocksx package is missing, downloads will not happen over tor"))
82                 md.set_position(gtk.WIN_POS_CENTER)
83                 md.run()
84                 md.destroy()
85                 self.common.settings['update_over_tor'] = False
86                 self.common.save_settings()
87
88         # is firefox already running?
89         if self.common.settings['installed_version']:
90             firefox_pid = self.common.get_pid('./Browser/firefox')
91             if firefox_pid:
92                 print _('Firefox is open, bringing to focus')
93                 # bring firefox to front
94                 self.common.bring_window_to_front(firefox_pid)
95                 return
96
97         # check for updates?
98         check_for_updates = False
99         if self.common.settings['check_for_updates']:
100             check_for_updates = True
101
102         if not check_for_updates:
103             # how long was it since the last update check?
104             # 86400 seconds = 24 hours
105             current_timestamp = int(time.time())
106             if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
107                 check_for_updates = True
108
109         if check_for_updates:
110             # check for update
111             print 'Checking for update'
112             self.set_gui('task', _("Checking for Tor Browser update."),
113                          ['download_update_check',
114                           'attempt_update'])
115         else:
116             # no need to check for update
117             print _('Checked for update within 24 hours, skipping')
118             self.start_launcher()
119
120         if self.launch_gui:
121             # set up the window
122             self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
123             self.window.set_title(_("Tor Browser"))
124             self.window.set_icon_from_file(self.common.paths['icon_file'])
125             self.window.set_position(gtk.WIN_POS_CENTER)
126             self.window.set_border_width(10)
127             self.window.connect("delete_event", self.delete_event)
128             self.window.connect("destroy", self.destroy)
129
130             # build the rest of the UI
131             self.build_ui()
132
133     # download or run TBB
134     def start_launcher(self):
135         # is TBB already installed?
136         latest_version = self.common.settings['latest_version']
137         installed_version = self.common.settings['installed_version']
138
139         # verify installed version for newer versions of TBB (#58)
140         if installed_version >= '3.0':
141             versions_filename = self.common.paths['tbb']['versions']
142             if os.path.exists(versions_filename):
143                 for line in open(versions_filename):
144                     if 'TORBROWSER_VERSION' in line:
145                         installed_version = line.lstrip('TORBROWSER_VERSION=').strip()
146
147         start = self.common.paths['tbb']['start']
148         if os.path.isfile(start) and os.access(start, os.X_OK):
149             if installed_version == latest_version:
150                 print _('Latest version of TBB is installed, launching')
151                 # current version of tbb is installed, launch it
152                 self.run(False)
153                 self.launch_gui = False
154             elif installed_version < latest_version:
155                 print _('TBB is out of date, attempting to upgrade to {0}'.format(latest_version))
156                 # there is a tbb upgrade available
157                 self.set_gui('task', _("Your Tor Browser is out of date. Upgrading from {0} to {1}.".format(installed_version, latest_version)),
158                              ['download_sha256',
159                               'download_sha256_sig',
160                               'download_tarball',
161                               'verify',
162                               'extract',
163                               'run'])
164             else:
165                 # for some reason the installed tbb is newer than the current version?
166                 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
167
168         # not installed
169         else:
170             print _('TBB is not installed, attempting to install {0}'.format(latest_version))
171             self.set_gui('task', _("Downloading and installing Tor Browser for the first time."),
172                          ['download_sha256',
173                           'download_sha256_sig',
174                           'download_tarball',
175                           'verify',
176                           'extract',
177                           'run'])
178
179     # there are different GUIs that might appear, this sets which one we want
180     def set_gui(self, gui, message, tasks, autostart=True):
181         self.gui = gui
182         self.gui_message = message
183         self.gui_tasks = tasks
184         self.gui_task_i = 0
185         self.gui_autostart = autostart
186
187     # set all gtk variables to False
188     def clear_ui(self):
189         if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
190             self.box.destroy()
191         self.box = False
192
193         self.label = False
194         self.progressbar = False
195         self.button_box = False
196         self.start_button = False
197         self.exit_button = False
198
199     # build the application's UI
200     def build_ui(self):
201         self.clear_ui()
202
203         self.box = gtk.VBox(False, 20)
204         self.window.add(self.box)
205
206         if 'error' in self.gui:
207             # labels
208             self.label = gtk.Label(self.gui_message)
209             self.label.set_line_wrap(True)
210             self.box.pack_start(self.label, True, True, 0)
211             self.label.show()
212
213             # button box
214             self.button_box = gtk.HButtonBox()
215             self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
216             self.box.pack_start(self.button_box, True, True, 0)
217             self.button_box.show()
218
219             if self.gui != 'error':
220                 # yes button
221                 yes_image = gtk.Image()
222                 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
223                 self.yes_button = gtk.Button("Yes")
224                 self.yes_button.set_image(yes_image)
225                 if self.gui == 'error_try_stable':
226                     self.yes_button.connect("clicked", self.try_stable, None)
227                 elif self.gui == 'error_try_default_mirror':
228                     self.yes_button.connect("clicked", self.try_default_mirror, None)
229                 elif self.gui == 'error_try_tor':
230                     self.yes_button.connect("clicked", self.try_tor, None)
231                 self.button_box.add(self.yes_button)
232                 self.yes_button.show()
233
234             # exit button
235             exit_image = gtk.Image()
236             exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
237             self.exit_button = gtk.Button("Exit")
238             self.exit_button.set_image(exit_image)
239             self.exit_button.connect("clicked", self.destroy, None)
240             self.button_box.add(self.exit_button)
241             self.exit_button.show()
242
243         elif self.gui == 'task':
244             # label
245             self.label = gtk.Label(self.gui_message)
246             self.label.set_line_wrap(True)
247             self.box.pack_start(self.label, True, True, 0)
248             self.label.show()
249
250             # progress bar
251             self.progressbar = gtk.ProgressBar(adjustment=None)
252             self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
253             self.progressbar.set_pulse_step(0.01)
254             self.box.pack_start(self.progressbar, True, True, 0)
255
256             # button box
257             self.button_box = gtk.HButtonBox()
258             self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
259             self.box.pack_start(self.button_box, True, True, 0)
260             self.button_box.show()
261
262             # start button
263             start_image = gtk.Image()
264             start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
265             self.start_button = gtk.Button(_("Start"))
266             self.start_button.set_image(start_image)
267             self.start_button.connect("clicked", self.start, None)
268             self.button_box.add(self.start_button)
269             if not self.gui_autostart:
270                 self.start_button.show()
271
272             # exit button
273             exit_image = gtk.Image()
274             exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
275             self.exit_button = gtk.Button(_("Exit"))
276             self.exit_button.set_image(exit_image)
277             self.exit_button.connect("clicked", self.destroy, None)
278             self.button_box.add(self.exit_button)
279             self.exit_button.show()
280
281         self.box.show()
282         self.window.show()
283
284         if self.gui_autostart:
285             self.start(None)
286
287     # start button clicked, begin tasks
288     def start(self, widget, data=None):
289         # disable the start button
290         if self.start_button:
291             self.start_button.set_sensitive(False)
292
293         # start running tasks
294         self.run_task()
295
296     # run the next task in the task list
297     def run_task(self):
298         self.refresh_gtk()
299
300         if self.gui_task_i >= len(self.gui_tasks):
301             self.destroy(False)
302             return
303
304         task = self.gui_tasks[self.gui_task_i]
305
306         # get ready for the next task
307         self.gui_task_i += 1
308
309         print _('Running task: {0}'.format(task))
310         if task == 'download_update_check':
311             print _('Downloading'), self.common.paths['update_check_url']
312             self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
313
314         if task == 'attempt_update':
315             print _('Checking to see if update is needed')
316             self.attempt_update()
317
318         elif task == 'download_sha256':
319             print _('Downloading'), self.common.paths['sha256_url'].format(self.common.settings['mirror'])
320             self.download('signature', self.common.paths['sha256_url'], self.common.paths['sha256_file'])
321
322         elif task == 'download_sha256_sig':
323             print _('Downloading'), self.common.paths['sha256_sig_url'].format(self.common.settings['mirror'])
324             self.download('signature', self.common.paths['sha256_sig_url'], self.common.paths['sha256_sig_file'])
325
326         elif task == 'download_tarball':
327             print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
328             self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
329
330         elif task == 'verify':
331             print _('Verifying signature')
332             self.verify()
333
334         elif task == 'extract':
335             print _('Extracting'), self.common.paths['tarball_filename']
336             self.extract()
337
338         elif task == 'run':
339             print _('Running'), self.common.paths['tbb']['start']
340             self.run()
341
342         elif task == 'start_over':
343             print _('Starting download over again')
344             self.start_over()
345
346     def response_received(self, response):
347         class FileDownloader(Protocol):
348             def __init__(self, common, file, url, total, progress, done_cb):
349                 self.file = file
350                 self.total = total
351                 self.so_far = 0
352                 self.progress = progress
353                 self.all_done = done_cb
354
355                 if response.code != 200:
356                     if common.settings['mirror'] != common.default_mirror:
357                         raise TryDefaultMirrorException(_("Download Error: {0} {1}\n\nYou are currently using a non-default mirror:\n{2}\n\nWould you like to switch back to the default?").format(response.code, response.phrase, common.settings['mirror']))
358                     else:
359                         raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
360
361             def dataReceived(self, bytes):
362                 self.file.write(bytes)
363                 self.so_far += len(bytes)
364                 percent = float(self.so_far) / float(self.total)
365                 self.progress.set_fraction(percent)
366                 amount = float(self.so_far)
367                 units = "bytes"
368                 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
369                     if amount > size:
370                         units = unit
371                         amount = amount / float(size)
372                         break
373
374                 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
375
376             def connectionLost(self, reason):
377                 print _('Finished receiving body:'), reason.getErrorMessage()
378                 self.all_done(reason)
379
380         if hasattr(self, 'current_download_url'):
381             url = self.current_download_url
382         else:
383             url = None
384
385         dl = FileDownloader(self.common, self.file_download, url, response.length, self.progressbar, self.response_finished)
386         response.deliverBody(dl)
387
388     def response_finished(self, msg):
389         if msg.check(ResponseDone):
390             self.file_download.close()
391             delattr(self, 'current_download_path')
392             delattr(self, 'current_download_url')
393
394             # next task!
395             self.run_task()
396
397         else:
398             print "FINISHED", msg
399             ## FIXME handle errors
400
401     def download_error(self, f):
402         print _("Download error:"), f.value, type(f.value)
403
404         if isinstance(f.value, TryStableException):
405             f.trap(TryStableException)
406             self.set_gui('error_try_stable', str(f.value), [], False)
407
408         elif isinstance(f.value, TryDefaultMirrorException):
409             f.trap(TryDefaultMirrorException)
410             self.set_gui('error_try_default_mirror', str(f.value), [], False)
411
412         elif isinstance(f.value, DownloadErrorException):
413             f.trap(DownloadErrorException)
414             self.set_gui('error', str(f.value), [], False)
415
416         elif isinstance(f.value, DNSLookupError):
417             f.trap(DNSLookupError)
418             if common.settings['mirror'] != common.default_mirror:
419                 self.set_gui('error_try_default_mirror', _("DNS Lookup Error\n\nYou are currently using a non-default mirror:\n{0}\n\nWould you like to switch back to the default?").format(common.settings['mirror']), [], False)
420             else:
421                 self.set_gui('error', str(f.value), [], False)
422
423         elif isinstance(f.value, ResponseFailed):
424             for reason in f.value.reasons:
425                 if isinstance(reason.value, OpenSSL.SSL.Error):
426                     # TODO: add the ability to report attack by posting bug to trac.torproject.org
427                     if not self.common.settings['update_over_tor']:
428                         self.set_gui('error_try_tor', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack. Try the download again using Tor?'), [], False)
429                     else:
430                         self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
431
432         else:
433             self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
434
435         self.build_ui()
436
437     def download(self, name, url, path):
438         # keep track of current download
439         self.current_download_path = path
440         self.current_download_url = url
441
442         # initialize the progress bar
443         mirror_url = url.format(self.common.settings['mirror'])
444         self.progressbar.set_fraction(0)
445         self.progressbar.set_text(_('Downloading {0}').format(name))
446         self.progressbar.show()
447         self.refresh_gtk()
448
449         if self.common.settings['update_over_tor']:
450             print _('Updating over Tor')
451             from twisted.internet.endpoints import TCP4ClientEndpoint
452             from txsocksx.http import SOCKS5Agent
453
454             torEndpoint = TCP4ClientEndpoint(reactor, '127.0.0.1', 9050)
455
456             # default mirror gets certificate pinning, only for requests that use the mirror
457             if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
458                 agent = SOCKS5Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']), proxyEndpoint=torEndpoint)
459             else:
460                 agent = SOCKS5Agent(reactor, proxyEndpoint=torEndpoint)
461         else:
462             if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
463                 agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
464             else:
465                 agent = Agent(reactor)
466
467         # actually, agent needs to follow redirect
468         agent = RedirectAgent(agent)
469
470         # start the request
471         d = agent.request('GET', mirror_url,
472                           Headers({'User-Agent': ['torbrowser-launcher']}),
473                           None)
474
475         self.file_download = open(path, 'w')
476         d.addCallback(self.response_received).addErrback(self.download_error)
477
478         if not reactor.running:
479             reactor.run()
480
481     def try_default_mirror(self, widget, data=None):
482         # change mirror to default and relaunch TBL
483         self.common.settings['mirror'] = self.common.default_mirror
484         self.common.save_settings()
485         subprocess.Popen([self.common.paths['tbl_bin']])
486         self.destroy(False)
487
488     def try_tor(self, widget, data=None):
489         # set update_over_tor to true and relaunch TBL
490         self.common.settings['update_over_tor'] = True
491         self.common.save_settings()
492         subprocess.Popen([self.common.paths['tbl_bin']])
493         self.destroy(False)
494
495     def attempt_update(self):
496         # load the update check file
497         try:
498             versions = json.load(open(self.common.paths['update_check_file']))
499             latest = None
500
501             # filter linux versions
502             valid = []
503             for version in versions:
504                 if '-Linux' in version:
505                     valid.append(str(version))
506             valid.sort()
507             if len(valid):
508                 versions = valid
509
510             if len(versions) == 1:
511                 latest = versions.pop()
512             else:
513                 stable = []
514                 # remove alphas/betas
515                 for version in versions:
516                     if not re.search(r'a\d-Linux', version) and not re.search(r'b\d-Linux', version):
517                         stable.append(version)
518                 if len(stable):
519                     latest = stable.pop()
520                 else:
521                     latest = versions.pop()
522
523             if latest:
524                 latest = str(latest)
525                 if latest.endswith('-Linux'):
526                     latest = latest.rstrip('-Linux')
527
528                 self.common.settings['latest_version'] = latest
529                 self.common.settings['last_update_check_timestamp'] = int(time.time())
530                 self.common.settings['check_for_updates'] = False
531                 self.common.save_settings()
532                 self.common.build_paths(self.common.settings['latest_version'])
533                 self.start_launcher()
534
535             else:
536                 # failed to find the latest version
537                 self.set_gui('error', _("Error checking for updates."), [], False)
538
539         except:
540             # not a valid JSON object
541             self.set_gui('error', _("Error checking for updates."), [], False)
542
543         # now start over
544         self.clear_ui()
545         self.build_ui()
546
547     def verify(self):
548         # initialize the progress bar
549         self.progressbar.set_fraction(0)
550         self.progressbar.set_text(_('Verifying Signature'))
551         self.progressbar.show()
552
553         verified = False
554         # check the sha256 file's sig, and also take the sha256 of the tarball and compare
555         p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']])
556         self.pulse_until_process_exits(p)
557         if p.returncode == 0:
558             # compare with sha256 of the tarball
559             tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest()
560             for line in open(self.common.paths['sha256_file'], 'r').readlines():
561                 if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line:
562                     verified = True
563
564         if verified:
565             self.run_task()
566         else:
567             # TODO: add the ability to report attack by posting bug to trac.torproject.org
568             self.set_gui('task', _("SIGNATURE VERIFICATION FAILED!\n\nYou might be under attack, or there might just be a networking problem. Click Start try the download again."), ['start_over'], False)
569             self.clear_ui()
570             self.build_ui()
571
572             if not reactor.running:
573                 reactor.run()
574
575     def extract(self):
576         # initialize the progress bar
577         self.progressbar.set_fraction(0)
578         self.progressbar.set_text(_('Installing'))
579         self.progressbar.show()
580         self.refresh_gtk()
581
582         extracted = False
583         try:
584             if self.common.paths['tarball_file'][-2:] == 'xz':
585                 # if tarball is .tar.xz
586                 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
587                 tf = tarfile.open(fileobj=xz)
588                 tf.extractall(self.common.paths['tbb']['dir'])
589                 extracted = True
590             else:
591                 # if tarball is .tar.gz
592                 if tarfile.is_tarfile(self.common.paths['tarball_file']):
593                     tf = tarfile.open(self.common.paths['tarball_file'])
594                     tf.extractall(self.common.paths['tbb']['dir'])
595                     extracted = True
596         except:
597             pass
598
599         if not extracted:
600             self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
601             self.clear_ui()
602             self.build_ui()
603             return
604
605         # installation is finished, so save installed_version
606         self.common.settings['installed_version'] = self.common.settings['latest_version']
607         self.common.save_settings()
608
609         self.run_task()
610
611     def run(self, run_next_task=True):
612         # play modem sound?
613         if self.common.settings['modem_sound']:
614             def play_modem_sound():
615                 try:
616                     import pygame
617                     pygame.mixer.init()
618                     sound = pygame.mixer.Sound(self.common.paths['modem_sound'])
619                     sound.play()
620                     time.sleep(10)
621                 except ImportError:
622                     md = gtk.MessageDialog(None, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, _("The python-pygame package is missing, the modem sound is unavailable."))
623                     md.set_position(gtk.WIN_POS_CENTER)
624                     md.run()
625                     md.destroy()
626
627             t = threading.Thread(target=play_modem_sound)
628             t.start()
629
630         # hide the TBL window (#151)
631         if hasattr(self, 'window'):
632             self.window.hide()
633             while gtk.events_pending():
634                 gtk.main_iteration_do(True)
635
636         # run Tor Browser
637         subprocess.call([self.common.paths['tbb']['start'], '--detach'])
638
639         if run_next_task:
640             self.run_task()
641
642     # make the progress bar pulse until process p (a Popen object) finishes
643     def pulse_until_process_exits(self, p):
644         while p.poll() is None:
645             time.sleep(0.01)
646             self.progressbar.pulse()
647             self.refresh_gtk()
648
649     # start over and download TBB again
650     def start_over(self):
651         self.label.set_text(_("Downloading Tor Browser Bundle over again."))
652         self.gui_tasks = ['download_tarball', 'verify', 'extract', 'run']
653         self.gui_task_i = 0
654         self.start(None)
655
656     # refresh gtk
657     def refresh_gtk(self):
658         while gtk.events_pending():
659             gtk.main_iteration(False)
660
661     # exit
662     def delete_event(self, widget, event, data=None):
663         return False
664
665     def destroy(self, widget, data=None):
666         if hasattr(self, 'file_download'):
667             self.file_download.close()
668         if hasattr(self, 'current_download_path'):
669             os.remove(self.current_download_path)
670             delattr(self, 'current_download_path')
671             delattr(self, 'current_download_url')
672         if reactor.running:
673             reactor.stop()