1# Copyright 2009 Google Inc. All Rights Reserved. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""Tk user interface implementation for namebench.""" 16 17__author__ = 'tstromberg@google.com (Thomas Stromberg)' 18 19import datetime 20import os 21import Queue 22import sys 23import threading 24import tkFont 25# Wildcard imports are evil. 26from Tkinter import * 27import tkMessageBox 28import traceback 29 30import addr_util 31import base_ui 32import conn_quality 33import nameserver_list 34import util 35 36THREAD_UNSAFE_TK = 0 37LOG_FILE_PATH = util.GenerateOutputFilename('log') 38 39 40def closedWindowHandler(): 41 print 'Au revoir, mes amis!' 42 sys.exit(1) 43 44global_message_queue = Queue.Queue() 45global_last_message = None 46 47 48def AddMsg(message, master=None, backup_notifier=None, **kwargs): 49 """Add a message to the global queue for output.""" 50 global global_message_queue 51 global global_last_message 52 global THREAD_UNSAFE_TK 53 54 new_message = StatusMessage(message, **kwargs) 55 if new_message != global_last_message: 56 global_message_queue.put(new_message) 57 58 if master: 59 try: 60 master.event_generate('<<msg>>', when='tail') 61 global_last_message = new_message 62 # Tk thread-safety workaround #1 63 except TclError: 64 # If we aren't thread safe, we already assume this won't work. 65 if not THREAD_UNSAFE_TK: 66 print 'First TCL Error:' 67 traceback.print_exc() 68 try: 69 backup_notifier(-1) 70 THREAD_UNSAFE_TK = 1 71 except: 72 print 'Backup notifier failure:' 73 traceback.print_exc() 74 75 76class StatusMessage(object): 77 """Messages to be passed from to the main thread from children. 78 79 Used to avoid thread issues inherent with Tk. 80 """ 81 82 def __init__(self, message, error=False, count=False, total=False, 83 enable_button=None, debug=False): 84 self.message = message 85 self.error = error 86 self.count = count 87 self.debug = debug 88 self.total = total 89 self.enable_button = enable_button 90 91 92class WorkerThread(threading.Thread, base_ui.BaseUI): 93 """Handle benchmarking and preparation in a separate UI thread.""" 94 95 def __init__(self, supplied_ns, global_ns, regional_ns, options, data_source=None, master=None, 96 backup_notifier=None): 97 threading.Thread.__init__(self) 98 self.SetupDataStructures() 99 self.status_callback = self.msg 100 self.data_src = data_source 101 self.backup_notifier = backup_notifier 102 self.include_internal = False 103 self.supplied_ns = supplied_ns 104 self.global_ns = global_ns 105 self.regional_ns = regional_ns 106 self.master = master 107 self.options = options 108 self.resource_dir = os.path.dirname(os.path.dirname(__file__)) 109 110 def msg(self, message, **kwargs): 111 """Add messages to the main queue.""" 112 return AddMsg(message, master=self.master, backup_notifier=self.backup_notifier, **kwargs) 113 114 def run(self): 115 self.msg('Started thread', enable_button=False) 116 try: 117 self.PrepareTestRecords() 118 self.PrepareNameServers() 119 self.PrepareBenchmark() 120 self.RunAndOpenReports() 121 except nameserver_list.OutgoingUdpInterception: 122 (exc_type, exception, tb) = sys.exc_info() 123 self.msg('Outgoing requests were intercepted!', error=exception) 124 except nameserver_list.TooFewNameservers: 125 (exc_type, exception, tb) = sys.exc_info() 126 self.msg('Too few nameservers to test', error=exception) 127 except conn_quality.OfflineConnection: 128 (exc_type, exception, tb) = sys.exc_info() 129 self.msg('The connection appears to be offline!', error=exception) 130 except: 131 (exc_type, exception, tb) = sys.exc_info() 132 traceback.print_exc(tb) 133 error_msg = '\n'.join(traceback.format_tb(tb)[-4:]) 134 self.msg(exception, error=error_msg) 135 self.msg(None, enable_button=True) 136 137 138class NameBenchGui(object): 139 """The main GUI.""" 140 141 def __init__(self, options, supplied_ns, global_ns, regional_ns, version=None): 142 self.options = options 143 self.supplied_ns = supplied_ns 144 self.global_ns = global_ns 145 self.regional_ns = regional_ns 146 self.version = version 147 148 def Execute(self): 149 self.root = Tk() 150 app = MainWindow(self.root, self.options, self.supplied_ns, self.global_ns, 151 self.regional_ns, self.version) 152 app.DrawWindow() 153 self.root.bind('<<msg>>', app.MessageHandler) 154 self.root.mainloop() 155 156 157class MainWindow(Frame, base_ui.BaseUI): 158 """The main Tk GUI class.""" 159 160 def __init__(self, master, options, supplied_ns, global_ns, regional_ns, version=None): 161 """TODO(tstromberg): Remove duplication from NameBenchGui class.""" 162 Frame.__init__(self) 163 self.SetupDataStructures() 164 self.master = master 165 self.options = options 166 self.supplied_ns = supplied_ns 167 self.global_ns = global_ns 168 self.regional_ns = regional_ns 169 self.version = version 170 try: 171 self.log_file = open(LOG_FILE_PATH, 'w') 172 except: 173 print 'Failed to open %s for write' % LOG_FILE_PATH 174 self.master.protocol('WM_DELETE_WINDOW', closedWindowHandler) 175 176 def UpdateStatus(self, message, count=None, total=None, error=None, debug=False): 177 """Update our little status window.""" 178 if not message: 179 return None 180 181 if total: 182 state = '%s... [%s/%s]' % (message, count, total) 183 elif count: 184 state = '%s%s' % (message, '.' * count) 185 else: 186 state = message 187 188 print '> %s' % str(state) 189 try: 190 self.log_file.write('%s: %s\r\n' % (datetime.datetime.now(), state)) 191 self.log_file.flush() 192 except: 193 pass 194 if not debug: 195 self.status.set(state[0:75]) 196 197 def DrawWindow(self): 198 """Draws the user interface.""" 199 self.nameserver_form = StringVar() 200 self.status = StringVar() 201 self.query_count = IntVar() 202 self.data_source = StringVar() 203 self.health_performance = StringVar() 204 self.location = StringVar() 205 self.use_global = IntVar() 206 self.use_regional = IntVar() 207 self.use_censor_checks = IntVar() 208 self.share_results = IntVar() 209 210 self.master.title('namebench') 211 outer_frame = Frame(self.master) 212 outer_frame.grid(row=0, padx=16, pady=16) 213 inner_frame = Frame(outer_frame, relief=GROOVE, bd=2, padx=12, pady=12) 214 inner_frame.grid(row=0, columnspan=2) 215 status = Label(outer_frame, text='...', textvariable=self.status) 216 status.grid(row=15, sticky=W, column=0) 217 218 if sys.platform[:3] == 'win': 219 seperator_width = 490 220 else: 221 seperator_width = 585 222 223 bold_font = tkFont.Font(font=status['font']) 224 bold_font['weight'] = 'bold' 225 226 ns_label = Label(inner_frame, text='Nameservers') 227 ns_label.grid(row=0, columnspan=2, sticky=W) 228 ns_label['font'] = bold_font 229 230 nameservers = Entry(inner_frame, bg='white', 231 textvariable=self.nameserver_form, 232 width=80) 233 nameservers.grid(row=1, columnspan=2, sticky=W, padx=4, pady=2) 234 self.nameserver_form.set(', '.join(nameserver_list.InternalNameServers())) 235 236 global_button = Checkbutton(inner_frame, 237 text='Include global DNS providers (Google Public DNS, OpenDNS, UltraDNS, etc.)', 238 variable=self.use_global) 239 global_button.grid(row=2, columnspan=2, sticky=W) 240 global_button.toggle() 241 242 regional_button = Checkbutton(inner_frame, 243 text='Include best available regional DNS services', 244 variable=self.use_regional) 245 regional_button.grid(row=3, columnspan=2, sticky=W) 246 regional_button.toggle() 247 248 separator = Frame(inner_frame, height=2, width=seperator_width, bd=1, relief=SUNKEN) 249 separator.grid(row=4, padx=5, pady=5, columnspan=2) 250 251 ds_label = Label(inner_frame, text='Options') 252 ds_label.grid(row=5, column=0, sticky=W) 253 ds_label['font'] = bold_font 254 255 censorship_button = Checkbutton(inner_frame, text='Include censorship checks', 256 variable=self.use_censor_checks) 257 censorship_button.grid(row=6, columnspan=2, sticky=W) 258 259 share_button = Checkbutton(inner_frame, 260 text='Upload and share your anonymized results (help speed up the internet!)', 261 variable=self.share_results) 262 263 # Old versions of Tk do not support two-dimensional padding. 264 try: 265 share_button.grid(row=7, columnspan=2, sticky=W, pady=[0,10]) 266 except TclError: 267 share_button.grid(row=7, columnspan=2, sticky=W) 268 269 loc_label = Label(inner_frame, text='Your location') 270 loc_label.grid(row=10, column=0, sticky=W) 271 loc_label['font'] = bold_font 272 273 run_count_label = Label(inner_frame, text='Health Check Performance') 274 run_count_label.grid(row=10, column=1, sticky=W) 275 run_count_label['font'] = bold_font 276 277 self.DiscoverLocation() 278 self.LoadDataSources() 279 source_titles = self.data_src.ListSourceTitles() 280 left_dropdown_width = max([len(x) for x in source_titles]) - 3 281 282 location_choices = [self.country, '(Other)'] 283 location = OptionMenu(inner_frame, self.location, *location_choices) 284 location.configure(width=left_dropdown_width) 285 location.grid(row=11, column=0, sticky=W) 286 self.location.set(location_choices[0]) 287 288 mode_choices = ['Fast', 'Slow (unstable network)'] 289 right_dropdown_width = max([len(x) for x in mode_choices]) - 3 290 health_performance = OptionMenu(inner_frame, self.health_performance, *mode_choices) 291 health_performance.configure(width=right_dropdown_width) 292 health_performance.grid(row=11, column=1, sticky=W) 293 self.health_performance.set(mode_choices[0]) 294 295 ds_label = Label(inner_frame, text='Query Data Source') 296 ds_label.grid(row=12, column=0, sticky=W) 297 ds_label['font'] = bold_font 298 299 numqueries_label = Label(inner_frame, text='Number of queries') 300 numqueries_label.grid(row=12, column=1, sticky=W) 301 numqueries_label['font'] = bold_font 302 303 data_source = OptionMenu(inner_frame, self.data_source, *source_titles) 304 data_source.configure(width=left_dropdown_width) 305 data_source.grid(row=13, column=0, sticky=W) 306 self.data_source.set(source_titles[0]) 307 308 query_count = Entry(inner_frame, bg='white', textvariable=self.query_count) 309 query_count.grid(row=13, column=1, sticky=W, padx=4) 310 query_count.configure(width=right_dropdown_width + 6) 311 self.query_count.set(self.options.query_count) 312 313 self.button = Button(outer_frame, command=self.StartJob) 314 self.button.grid(row=15, sticky=E, column=1, pady=4, padx=1) 315 self.UpdateRunState(running=True) 316 self.UpdateRunState(running=False) 317 self.UpdateStatus('namebench %s is ready!' % self.version) 318 319 def MessageHandler(self, unused_event): 320 """Pinged when there is a new message in our queue to handle.""" 321 while global_message_queue.qsize(): 322 m = global_message_queue.get() 323 if m.error: 324 self.ErrorPopup(m.message, m.error) 325 elif m.enable_button == False: 326 self.UpdateRunState(running=True) 327 elif m.enable_button == True: 328 self.UpdateRunState(running=False) 329 self.UpdateStatus(m.message, count=m.count, total=m.total, error=m.error, debug=m.debug) 330 331 def ErrorPopup(self, title, message): 332 print 'Showing popup: %s' % title 333 tkMessageBox.showerror(str(title), str(message), master=self.master) 334 335 def UpdateRunState(self, running=True): 336 """Update the run state of the window, using nasty threading hacks.""" 337 338 global THREAD_UNSAFE_TK 339 # try/except blocks added to work around broken Tcl/Tk libraries 340 # shipped with Fedora 11 (not thread-safe). 341 # See http://code.google.com/p/namebench/issues/detail?id=23' 342 if THREAD_UNSAFE_TK: 343 return 344 345 if running: 346 try: 347 self.button.config(state=DISABLED) 348 self.button.config(text='Running') 349 except TclError: 350 THREAD_UNSAFE_TK = True 351 self.UpdateStatus('Unable to disable button due to broken Tk library') 352 self.UpdateStatus('Running...') 353 else: 354 try: 355 self.button.config(state=NORMAL) 356 self.button.config(text='Start Benchmark') 357 except TclError: 358 pass 359 360 def StartJob(self): 361 """Events that get called when the Start button is pressed.""" 362 363 self.ProcessForm() 364 thread = WorkerThread(self.supplied_ns, self.global_ns, self.regional_ns, self.options, 365 data_source=self.data_src, 366 master=self.master, backup_notifier=self.MessageHandler) 367 thread.start() 368 369 def ProcessForm(self): 370 """Read form and populate instance variables.""" 371 372 self.supplied_ns = addr_util.ExtractIPTuplesFromString(self.nameserver_form.get()) 373 if not self.use_global.get(): 374 self.global_ns = [] 375 if not self.use_regional.get(): 376 self.regional_ns = [] 377 378 if 'Slow' in self.health_performance.get(): 379 self.options.health_thread_count = 10 380 381 self.options.query_count = self.query_count.get() 382 self.options.input_source = self.data_src.ConvertSourceTitleToType(self.data_source.get()) 383 self.options.enable_censorship_checks = self.use_censor_checks.get() 384 self.options.upload_results = self.share_results.get() 385