]> git.lizzy.rs Git - torbrowser-launcher.git/blob - torbrowser-launcher
refactored a bunch. separated GUI code from other common logic, to make way for a...
[torbrowser-launcher.git] / torbrowser-launcher
1 #!/usr/bin/env python
2 """
3 Tor Browser Launcher
4 https://github.com/micahflee/torbrowser-launcher/
5
6 Copyright (c) 2013 Micah Lee <micahflee@riseup.net>
7
8 Permission is hereby granted, free of charge, to any person
9 obtaining a copy of this software and associated documentation
10 files (the "Software"), to deal in the Software without
11 restriction, including without limitation the rights to use,
12 copy, modify, merge, publish, distribute, sublicense, and/or sell
13 copies of the Software, and to permit persons to whom the
14 Software is furnished to do so, subject to the following
15 conditions:
16
17 The above copyright notice and this permission notice shall be
18 included in all copies or substantial portions of the Software.
19
20 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
22 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
24 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
25 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27 OTHER DEALINGS IN THE SOFTWARE.
28 """
29
30 import gettext
31 gettext.install('torbrowser-launcher', '/usr/share/torbrowser-launcher/locale')
32
33 from twisted.internet import gtk2reactor
34 gtk2reactor.install()
35 from twisted.internet import reactor
36
37 import pygtk
38 pygtk.require('2.0')
39 import gtk
40
41 import os, sys, subprocess, locale, urllib2, gobject, time, pickle, json, tarfile, psutil
42
43 from twisted.web.client import Agent, ResponseDone
44 from twisted.web.http_headers import Headers
45 from twisted.internet.protocol import Protocol
46 from twisted.internet.ssl import ClientContextFactory
47
48 from OpenSSL.SSL import Context, VERIFY_PEER, VERIFY_FAIL_IF_NO_PEER_CERT
49 from OpenSSL.crypto import load_certificate, FILETYPE_PEM
50
51 class VerifyTorProjectCert(ClientContextFactory):
52
53     def __init__(self, torproject_pem):
54         self.torproject_ca = load_certificate(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(VERIFY_PEER | 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 TBLCommon:
66
67     def __init__(self):
68         print _('Initializing Tor Browser Launcher')
69         
70         # initialize the app
71         self.discover_arch_lang()
72         self.build_paths()
73         self.mkdir(self.paths['dir']['download'])
74         self.mkdir(self.paths['dir']['tbb'])
75         self.init_gnupg()
76
77         # allow buttons to have icons
78         try:
79             settings = gtk.settings_get_default()
80             settings.props.gtk_button_images = True
81         except:
82             pass
83
84     # discover the architecture and language
85     def discover_arch_lang(self):
86         # figure out the architecture
87         (sysname, nodename, release, version, machine) = os.uname()
88         self.architecture = machine
89
90         # figure out the language
91         available_languages = ['en-US', 'ar', 'de', 'es-ES', 'fa', 'fr', 'it', 'ko', 'nl', 'pl', 'pt-PT', 'ru', 'vi', 'zh-CN']
92         default_locale = locale.getdefaultlocale()[0]
93         if default_locale == None:
94             self.language = 'en-US'
95         else:
96             self.language = default_locale.replace('_', '-')
97             if self.language not in available_languages:
98                 self.language = self.language.split('-')[0]
99                 if self.language not in available_languages:
100                     for l in available_languages:
101                         if l[0:2] == self.language:
102                             self.language = l
103             # if language isn't available, default to english
104             if self.language not in available_languages:
105                 self.language = 'en-US'
106
107     # build all relevant paths
108     def build_paths(self, tbb_version = None):
109         homedir = os.getenv('HOME')
110         if not homedir:
111             homedir = '/tmp/.torbrowser-'+os.getenv('USER')
112             if os.path.exists(homedir) == False:
113                 try:
114                     os.mkdir(homedir, 0700)
115                 except:
116                     self.set_gui('error', _("Error creating {0}").format(homedir), [], False)
117         if not os.access(homedir, os.W_OK):
118             self.set_gui('error', _("{0} is not writable").format(homedir), [], False)
119
120         tbb_data = '%s/.torbrowser' % homedir
121
122         if tbb_version:
123             tarball_filename = 'tor-browser-gnu-linux-'+self.architecture+'-'+tbb_version+'-dev-'+self.language+'.tar.gz'
124             self.paths['file']['tarball'] = tbb_data+'/download/'+tarball_filename
125             self.paths['file']['tarball_sig'] = tbb_data+'/download/'+tarball_filename+'.asc'
126             self.paths['url']['tarball'] = 'https://www.torproject.org/dist/torbrowser/linux/'+tarball_filename
127             self.paths['url']['tarball_sig'] = 'https://www.torproject.org/dist/torbrowser/linux/'+tarball_filename+'.asc'
128             self.paths['filename']['tarball'] = tarball_filename
129             self.paths['filename']['tarball_sig'] = tarball_filename+'.asc'
130
131         else:
132             self.paths = {
133                 'dir': {
134                     'data': tbb_data,
135                     'download': tbb_data+'/download',
136                     'tbb': tbb_data+'/tbb/'+self.architecture,
137                     'gnupg_homedir': tbb_data+'/gnupg_homedir'
138                 },
139                 'file': {
140                     'tbl_bin': '/usr/bin/torbrowser-launcher',
141                     'settings': tbb_data+'/settings',
142                     'version': tbb_data+'/version',
143                     'start': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
144                     'vidalia_bin': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/App/vidalia',
145                     'firefox_bin': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/App/Firefox/firefox',
146                     'firefox_profile': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/Data/profile',
147                     'update_check': tbb_data+'/download/RecommendedTBBVersions',
148                     'icon': '/usr/share/pixmaps/torbrowser80.xpm',
149                     'torproject_pem': '/usr/share/torbrowser-launcher/torproject.pem',
150                     'erinn_key': '/usr/share/torbrowser-launcher/erinn.asc',
151                     'sebastian_key': '/usr/share/torbrowser-launcher/sebastian.asc'
152                 },
153                 'url': {
154                     'update_check': 'https://check.torproject.org/RecommendedTBBVersions'
155                 },
156                 'filename': {}
157             }
158
159     # create a directory
160     def mkdir(self, path):
161         try:
162             if os.path.exists(path) == False:
163                 os.makedirs(path, 0700)
164                 return True
165         except:
166             self.set_gui('error', _("Cannot create directory {0}").format(path), [], False)
167             return False
168         if not os.access(path, os.W_OK):
169             self.set_gui('error', _("{0} is not writable").format(path), [], False)
170             return False
171         return True
172
173     # if gnupg_homedir isn't set up, set it up
174     def init_gnupg(self):
175         if not os.path.exists(self.paths['dir']['gnupg_homedir']):
176             print _('Creating GnuPG homedir'), self.paths['dir']['gnupg_homedir']
177             if self.mkdir(self.paths['dir']['gnupg_homedir']):
178                 # import keys
179                 print _('Importing keys')
180                 p1 = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['dir']['gnupg_homedir'], '--import', self.paths['file']['erinn_key']])
181                 p1.wait()
182                 p2 = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['dir']['gnupg_homedir'], '--import', self.paths['file']['sebastian_key']])
183                 p2.wait()
184
185     # load settings
186     def load_settings(self):
187         if os.path.isfile(self.paths['file']['settings']):
188             self.settings = pickle.load(open(self.paths['file']['settings']))
189             # sanity checks
190             if not 'installed_version' in self.settings:
191                 return False
192             if not 'latest_version' in self.settings:
193                 return False
194             if not 'last_update_check_timestamp' in self.settings:
195                 return False
196         else:
197             self.settings = {
198                 'installed_version': False,
199                 'latest_version': '0',
200                 'last_update_check_timestamp': 0
201             }
202             self.save_settings()
203         return True
204
205     # save settings
206     def save_settings(self):
207         pickle.dump(self.settings, open(self.paths['file']['settings'], 'w'))
208         return True
209
210     # get the process id of a program
211     def get_pid(self, bin_path, python = False):
212         pid = None
213
214         for p in psutil.process_iter():
215             try:
216                 if p.pid != os.getpid():
217                     exe = None
218                     if python:
219                         if len(p.cmdline) > 1:
220                             if 'python' in p.cmdline[0]:
221                                 exe = p.cmdline[1]
222                     else:
223                         if len(p.cmdline) > 0:
224                             exe = p.cmdline[0]
225                     
226                     if exe == bin_path:
227                         pid = p.pid
228
229             except:
230                 pass
231
232         return pid
233
234     # bring program's x window to front
235     def bring_window_to_front(self, pid):
236         # figure out the window id
237         win_id = None
238         p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
239         for line in p.stdout.readlines():
240             line_split = line.split()
241             cur_win_id = line_split[0]
242             cur_win_pid = int(line_split[2])
243             if cur_win_pid == pid:
244                 win_id = cur_win_id
245
246         # bring to front
247         if win_id:
248             subprocess.call(['wmctrl', '-i', '-a', win_id])
249
250 class TBLSettings:
251     def __init__(self, common):
252         print _('Starting settings dialog')
253         self.common = common
254
255 class TBLLauncher:
256     def __init__(self, common):
257         print _('Starting launcher dialog')
258         self.common = common
259
260         self.set_gui(None, '', [])
261         self.launch_gui = True
262
263         # if we haven't already hit an error
264         if self.gui != 'error':
265             # load settings
266             if self.common.load_settings():
267                 self.common.build_paths(self.common.settings['latest_version'])
268
269                 # is vidalia already running and we just need to open a new firefox?
270                 if self.common.settings['installed_version']:
271                     vidalia_pid = self.common.get_pid('./App/vidalia')
272                     firefox_pid = self.common.get_pid(self.common.paths['file']['firefox_bin'])
273
274                     if vidalia_pid and not firefox_pid:
275                         print _('Vidalia is already open, but Firefox is closed. Launching new Firefox.')
276                         self.common.bring_window_to_front(vidalia_pid)
277                         subprocess.Popen([self.common.paths['file']['firefox_bin'], '-no-remote', '-profile', self.common.paths['file']['firefox_profile']])
278                         return
279                     elif vidalia_pid and firefox_pid:
280                         print _('Vidalia and Firefox are already open, bringing them to focus')
281
282                         # bring firefox to front, then vidalia
283                         self.common.bring_window_to_front(firefox_pid)
284                         self.common.bring_window_to_front(vidalia_pid)
285                         return
286
287                 # how long was it since the last update check?
288                 # 86400 seconds = 24 hours
289                 current_timestamp = int(time.time())
290                 if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
291                     # check for update
292                     print 'Checking for update'
293                     self.set_gui('task', _("Checking for Tor Browser update."), 
294                         ['download_update_check', 
295                          'attempt_update'])
296
297                 else:
298                     # no need to check for update
299                     print _('Checked for update within 24 hours, skipping')
300                     self.start_launcher()
301
302             else:
303                 self.set_gui('error', _("Error loading settings. Delete ~/.torbrowser and try again."), [])
304
305         if self.launch_gui:
306             # set up the window
307             self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
308             self.window.set_title(_("Tor Browser"))
309             self.window.set_icon_from_file(self.common.paths['file']['icon'])
310             self.window.set_position(gtk.WIN_POS_CENTER)
311             self.window.set_border_width(10)
312             self.window.connect("delete_event", self.delete_event)
313             self.window.connect("destroy", self.destroy)
314
315             # build the rest of the UI
316             self.build_ui()
317
318     # download or run TBB
319     def start_launcher(self):
320       # is TBB already installed?
321       if os.path.isfile(self.common.paths['file']['start']) and os.access(self.common.paths['file']['start'], os.X_OK):
322         if self.common.settings['installed_version'] == self.common.settings['latest_version']:
323           # current version of tbb is installed, launch it
324           self.run(False)
325           self.launch_gui = False
326         elif self.common.settings['installed_version'] < self.common.settings['latest_version']:
327           # there is a tbb upgrade available
328           self.set_gui('task', _("Your Tor Browser is out of date."), 
329             ['download_tarball', 
330              'download_tarball_sig', 
331              'verify', 
332              'extract', 
333              'run'])
334         else:
335           # for some reason the installed tbb is newer than the current version?
336           self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
337
338       # not installed
339       else:
340           # are the tarball and sig already downloaded?
341           if os.path.isfile(self.common.paths['file']['tarball']) and os.path.isfile(self.common.paths['file']['tarball_sig']):
342               # start the gui with verify
343               self.set_gui('task', _("Installing Tor Browser."), 
344                   ['verify', 
345                    'extract', 
346                    'run'])
347
348           # first run
349           else:
350               self.set_gui('task', _("Downloading and installing Tor Browser."), 
351                   ['download_tarball', 
352                    'download_tarball_sig', 
353                    'verify', 
354                    'extract', 
355                    'run'])
356    
357     # there are different GUIs that might appear, this sets which one we want
358     def set_gui(self, gui, message, tasks, autostart=True):
359         self.gui = gui
360         self.gui_message = message
361         self.gui_tasks = tasks
362         self.gui_task_i = 0
363         self.gui_autostart = autostart
364
365     # set all gtk variables to False
366     def clear_ui(self):
367         if hasattr(self, 'box'):
368             self.box.destroy()
369         self.box = False
370
371         self.label = False
372         self.progressbar = False
373         self.button_box = False
374         self.start_button = False
375         self.exit_button = False
376
377     # build the application's UI
378     def build_ui(self):
379         self.box = gtk.VBox(False, 20)
380         self.window.add(self.box)
381
382         if self.gui == 'error':
383             # labels
384             self.label = gtk.Label( self.gui_message ) 
385             self.label.set_line_wrap(True)
386             self.box.pack_start(self.label, True, True, 0)
387             self.label.show()
388
389             # exit button
390             exit_image = gtk.Image()
391             exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
392             self.exit_button = gtk.Button("Exit")
393             self.exit_button.set_image(exit_image)
394             self.exit_button.connect("clicked", self.destroy, None)
395             self.box.add(self.exit_button)
396             self.exit_button.show()
397
398         elif self.gui == 'task':
399             # label
400             self.label = gtk.Label( self.gui_message ) 
401             self.label.set_line_wrap(True)
402             self.box.pack_start(self.label, True, True, 0)
403             self.label.show()
404             
405             # progress bar
406             self.progressbar = gtk.ProgressBar(adjustment=None)
407             self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
408             self.progressbar.set_pulse_step(0.01)
409             self.box.pack_start(self.progressbar, True, True, 0)
410
411             # button box
412             self.button_box = gtk.HButtonBox()
413             self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
414             self.box.pack_start(self.button_box, True, True, 0)
415             self.button_box.show()
416
417             # start button
418             start_image = gtk.Image()
419             start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
420             self.start_button = gtk.Button("Start")
421             self.start_button.set_image(start_image)
422             self.start_button.connect("clicked", self.start, None)
423             self.button_box.add(self.start_button)
424             if not self.gui_autostart:
425               self.start_button.show()
426
427             # exit button
428             exit_image = gtk.Image()
429             exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
430             self.exit_button = gtk.Button("Exit")
431             self.exit_button.set_image(exit_image)
432             self.exit_button.connect("clicked", self.destroy, None)
433             self.button_box.add(self.exit_button)
434             self.exit_button.show()
435
436         self.box.show()
437         self.window.show()
438
439         if self.gui_autostart:
440             self.start(None)
441
442     # start button clicked, begin tasks
443     def start(self, widget, data=None):
444         # disable the start button
445         if self.start_button:
446             self.start_button.set_sensitive(False)
447
448         # start running tasks
449         self.run_task()
450       
451     # run the next task in the task list
452     def run_task(self):
453         self.refresh_gtk()
454
455         if self.gui_task_i >= len(self.gui_tasks):
456             self.destroy(False)
457             return
458
459         task = self.gui_tasks[self.gui_task_i]
460         
461         # get ready for the next task
462         self.gui_task_i += 1
463
464         if task == 'download_update_check':
465             print _('Downloading'), self.common.paths['url']['update_check']
466             self.download('update check', self.common.paths['url']['update_check'], self.common.paths['file']['update_check'])
467         
468         if task == 'attempt_update':
469             print _('Checking to see if update it needed')
470             self.attempt_update()
471
472         elif task == 'download_tarball':
473             print _('Downloading'), self.common.paths['url']['tarball']
474             self.download('tarball', self.common.paths['url']['tarball'], self.common.paths['file']['tarball'])
475
476         elif task == 'download_tarball_sig':
477             print _('Downloading'), self.common.paths['url']['tarball_sig']
478             self.download('signature', self.common.paths['url']['tarball_sig'], self.common.paths['file']['tarball_sig'])
479
480         elif task == 'verify':
481             print _('Verifying signature')
482             self.verify()
483
484         elif task == 'extract':
485             print _('Extracting'), self.common.paths['filename']['tarball']
486             self.extract()
487
488         elif task == 'run':
489             print _('Running'), self.common.paths['file']['start']
490             self.run()
491         
492         elif task == 'start_over':
493             print _('Starting download over again')
494             self.start_over()
495
496     def response_received(self, response):
497         class FileDownloader(Protocol):
498             def __init__(self, file, total, progress, done_cb):
499                 self.file = file
500                 self.total = total
501                 self.so_far = 0
502                 self.progress = progress
503                 self.all_done = done_cb
504
505             def dataReceived(self, bytes):
506                 self.file.write(bytes)
507                 self.so_far += len(bytes)
508                 percent = float(self.so_far) / float(self.total)
509                 self.progress.set_fraction(percent)
510                 amount = float(self.so_far)
511                 units = "bytes"
512                 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
513                     if amount > size:
514                         units = unit
515                         amount = amount / float(size)
516                         break
517
518                 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
519
520             def connectionLost(self, reason):
521                 print _('Finished receiving body:'), reason.getErrorMessage()
522                 self.all_done(reason)
523
524         dl = FileDownloader(self.file_download, response.length, self.progressbar, self.response_finished)
525         response.deliverBody(dl)
526
527     def response_finished(self, msg):
528         if msg.check(ResponseDone):
529             self.file_download.close()
530             # next task!
531             self.run_task()
532
533         else:
534             print "FINISHED", msg
535             ## FIXME handle errors
536
537     def download_error(self, f):
538         print _("Download error"), f
539         self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
540         self.clear_ui()
541         self.build_ui()
542
543     def download(self, name, url, path):
544         # initialize the progress bar
545         self.progressbar.set_fraction(0) 
546         self.progressbar.set_text(_('Downloading {0}').format(name))
547         self.progressbar.show()
548         self.refresh_gtk()
549
550         agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['file']['torproject_pem']))
551         d = agent.request('GET', url,
552                           Headers({'User-Agent': ['torbrowser-launcher']}),
553                           None)
554
555         self.file_download = open(path, 'w')
556         d.addCallback(self.response_received).addErrback(self.download_error)
557         
558         if not reactor.running:
559             reactor.run()
560
561     def attempt_update(self):
562         # load the update check file
563         try:
564             versions = json.load(open(self.common.paths['file']['update_check']))
565             latest_version = None
566
567             end = '-Linux'
568             for version in versions:
569                 if str(version).find(end) != -1:
570                     latest_version = str(version)
571
572             if latest_version:
573                 self.common.settings['latest_version'] = latest_version[:-len(end)]
574                 self.common.settings['last_update_check_timestamp'] = int(time.time())
575                 self.common.save_settings()
576                 self.common.build_paths(self.common.settings['latest_version'])
577                 self.start_launcher()
578
579             else:
580                 # failed to find the latest version
581                 self.set_gui('error', _("Error checking for updates."), [], False)
582         
583         except:
584             # not a valid JSON object
585             self.set_gui('error', _("Error checking for updates."), [], False)
586
587         # now start over
588         self.clear_ui()
589         self.build_ui()
590
591     def verify(self):
592         # initialize the progress bar
593         self.progressbar.set_fraction(0) 
594         self.progressbar.set_text(_('Verifying Signature'))
595         self.progressbar.show()
596
597         p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['dir']['gnupg_homedir'], '--verify', self.common.paths['file']['tarball_sig']])
598         self.pulse_until_process_exits(p)
599         
600         if p.returncode == 0:
601             self.run_task()
602         else:
603             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)
604             self.clear_ui()
605             self.build_ui()
606
607             if not reactor.running:
608                 reactor.run()
609
610     def extract(self):
611         # initialize the progress bar
612         self.progressbar.set_fraction(0) 
613         self.progressbar.set_text(_('Installing'))
614         self.progressbar.show()
615         self.refresh_gtk()
616
617         # make sure this file is a tarfile
618         if tarfile.is_tarfile(self.common.paths['file']['tarball']):
619           tf = tarfile.open(self.common.paths['file']['tarball'])
620           tf.extractall(self.common.paths['dir']['tbb'])
621         else:
622             self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}"), ['start_over'], False)
623             self.clear_ui()
624             self.build_ui()
625
626         # installation is finished, so save installed_version
627         self.common.settings['installed_version'] = self.common.settings['latest_version']
628         self.common.save_settings()
629
630         self.run_task()
631
632     def run(self, run_next_task = True):
633         subprocess.Popen([self.common.paths['file']['start']])
634         if run_next_task:
635             self.run_task()
636
637     # make the progress bar pulse until process p (a Popen object) finishes
638     def pulse_until_process_exits(self, p):
639         while p.poll() == None:
640             time.sleep(0.01)
641             self.progressbar.pulse()
642             self.refresh_gtk()
643
644     # start over and download TBB again
645     def start_over(self):
646         self.label.set_text(_("Downloading Tor Browser Bundle over again."))
647         self.gui_tasks = ['download_tarball', 'download_tarball_sig', 'verify', 'extract', 'run']
648         self.gui_task_i = 0
649         self.start(None)
650    
651     # refresh gtk
652     def refresh_gtk(self):
653         while gtk.events_pending():
654             gtk.main_iteration(False)
655
656     # exit
657     def delete_event(self, widget, event, data=None):
658         return False
659     def destroy(self, widget, data=None):
660         if hasattr(self, 'file_download'):
661             self.file_download.close()
662         if reactor.running:
663             reactor.stop()
664
665 if __name__ == "__main__":
666     tor_browser_launcher_version = '0.0.1'
667
668     print _('Tor Browser Launcher')
669     print _('By Micah Lee, licensed under GPLv3')
670     print _('version {0}').format(tor_browser_launcher_version)
671     print 'https://github.com/micahflee/torbrowser-launcher'
672
673     common = TBLCommon()
674
675     # is torbrowser-launcher already running?
676     tbl_pid = common.get_pid(common.paths['file']['tbl_bin'], True)
677     if tbl_pid:
678         print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
679         common.bring_window_to_front(tbl_pid)
680         sys.exit()
681
682     if '-settings' in sys.argv:
683         # settings mode
684         app = TBLSettings(common)
685
686     else:
687         # launcher mode
688         app = TBLLauncher(common)
689