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
16"""Cocoa frontend implementation for namebench."""
17
18__author__ = 'tstromberg@google.com (Thomas Stromberg)'
19
20
21import os
22import sys
23import traceback
24from Foundation import *
25from AppKit import *
26from objc import IBAction, IBOutlet
27
28from libnamebench import addr_util
29from libnamebench import base_ui
30from libnamebench import config
31from libnamebench import conn_quality
32from libnamebench import nameserver_list
33from libnamebench import util
34from libnamebench import version
35
36# How much room do we have in the UI for status messages?
37MAX_STATUS_LENGTH = 68
38
39
40class controller(NSWindowController, base_ui.BaseUI):
41  """Controller class associated with the main window."""
42  nameserver_form = IBOutlet()
43  include_global = IBOutlet()
44  include_regional = IBOutlet()
45  include_censorship_checks = IBOutlet()
46  data_source = IBOutlet()
47  health_performance = IBOutlet()
48  enable_sharing = IBOutlet()
49  location = IBOutlet()
50  query_count = IBOutlet()
51  run_count = IBOutlet()
52  status = IBOutlet()
53  spinner = IBOutlet()
54  button = IBOutlet()
55
56  def awakeFromNib(self):
57    """Initializes our class, called automatically by Cocoa."""
58    self.SetupDataStructures()
59    self.resource_dir = os.path.join(os.getcwd(), 'namebench.app', 'Contents', 'Resources')
60
61    conf_file = util.FindDataFile('config/namebench.cfg')
62    (self.options, self.supplied_ns, self.global_ns, self.regional_ns) = config.GetConfiguration(filename=conf_file)
63    # TODO(tstromberg): Consider moving this into a thread for faster loading.
64    self.UpdateStatus('Discovering sources')
65    self.LoadDataSources()
66    self.UpdateStatus('Discovering location')
67    self.DiscoverLocation()
68    self.UpdateStatus('Populating Form...')
69    self.setFormDefaults()
70    self.UpdateStatus('namebench %s is ready!' % version.VERSION)
71
72  @IBAction
73  def startJob_(self, sender):
74    """Trigger for the 'Start Benchmark' button, starts benchmark thread."""
75    self.ProcessForm()
76    self.UpdateStatus('Starting benchmark thread')
77    t = NSThread.alloc().initWithTarget_selector_object_(self, self.benchmarkThread, None)
78    t.start()
79
80  def UpdateStatus(self, message, count=None, total=None, error=False, debug=False):
81    """Update the status message at the bottom of the window."""
82    if error:
83      return self.displayError(message, error)
84    if total:
85      state = '%s [%s/%s]' % (message, count, total)
86    elif count:
87      state = '%s%s' % (message, '.' * count)
88    else:
89      state = message
90
91    state = state.replace('%', '%%')
92    print state
93    NSLog(state)
94
95    self.status.setStringValue_(state[0:MAX_STATUS_LENGTH])
96
97  def ProcessForm(self):
98    """Parse the form fields and populate class variables."""
99    self.UpdateStatus('Processing form inputs')
100    self.preferred = self.supplied_ns
101    self.include_internal = False
102
103    if not int(self.include_global.stringValue()):
104      self.UpdateStatus('Not using global')
105      self.global_ns = []
106    else:
107      self.preferred.extend(self.global_ns)
108    if not int(self.include_regional.stringValue()):
109      self.UpdateStatus('Not using regional')
110      self.regional_ns = []
111
112    if int(self.enable_sharing.stringValue()):
113      self.options.upload_results = True
114
115    if int(self.include_censorship_checks.stringValue()):
116      self.options.enable_censorship_checks = True
117
118    print self.health_performance.titleOfSelectedItem()
119    if 'Slow' in self.health_performance.titleOfSelectedItem():
120      self.options.health_thread_count = 10
121
122    self.options.input_source = self.data_src.ConvertSourceTitleToType(self.data_source.titleOfSelectedItem())
123    self.UpdateStatus('Supplied servers: %s' % self.nameserver_form.stringValue())
124    self.preferred.extend(addr_util.ExtractIPTuplesFromString(self.nameserver_form.stringValue()))
125    self.options.query_count = int(self.query_count.stringValue())
126
127  def benchmarkThread(self):
128    """Run the benchmarks, designed to be run in a thread."""
129    pool = NSAutoreleasePool.alloc().init()
130    self.spinner.startAnimation_(self)
131    self.button.setEnabled_(False)
132    self.UpdateStatus('Preparing benchmark')
133    try:
134      self.PrepareTestRecords()
135      self.PrepareNameServers()
136      self.PrepareBenchmark()
137      self.RunAndOpenReports()
138    except nameserver_list.OutgoingUdpInterception:
139      (exc_type, exception, tb) = sys.exc_info()
140      self.UpdateStatus('Outgoing requests were intercepted!',
141                        error=str(exception))
142    except nameserver_list.TooFewNameservers:
143      (exc_type, exception, tb) = sys.exc_info()
144      self.UpdateStatus('Too few nameservers to test', error=str(exception))
145    except conn_quality.OfflineConnection:
146      (exc_type, exception, tb) = sys.exc_info()
147      self.UpdateStatus('The connection appears to be offline!', error=str(exception))
148    except:
149      (exc_type, exception, tb) = sys.exc_info()
150      traceback.print_exc(tb)
151      error_msg = '\n'.join(traceback.format_tb(tb)[-4:])
152      self.UpdateStatus('FAIL: %s' % exception, error=error_msg)
153
154    self.spinner.stopAnimation_(self)
155    self.button.setEnabled_(True)
156    # This seems weird, but recommended by http://pyobjc.sourceforge.net/documentation/pyobjc-core/intro.html
157    del pool
158
159  def displayError(self, msg, details):
160    """Display an alert drop-down message."""
161    NSLog('ERROR: %s - %s' % (msg, details))
162    alert = NSAlert.alloc().init()
163    alert.setMessageText_(msg)
164    alert.setInformativeText_(details)
165    buttonPressed = alert.runModal()
166
167  def setFormDefaults(self):
168    """Set up the form with sane initial values."""
169    nameservers_string = ', '.join(nameserver_list.InternalNameServers())
170    self.nameserver_form.setStringValue_(nameservers_string)
171    self.query_count.setStringValue_(self.options.query_count)
172    self.query_count.setStringValue_(self.options.query_count)
173
174    self.location.removeAllItems()
175    if self.country:
176      self.location.addItemWithTitle_(self.country)
177      self.location.addItemWithTitle_('(Other)')
178    else:
179      self.location.addItemWithTitle_('(automatic)')
180
181    self.health_performance.removeAllItems()
182    self.health_performance.addItemWithTitle_('Fast')
183    self.health_performance.addItemWithTitle_('Slow (unstable network)')
184
185    self.data_source.removeAllItems()
186    self.data_source.addItemsWithTitles_(self.data_src.ListSourceTitles())
187
188