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