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