bastd.ui.settings.nettesting
Provides ui for network related testing.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Provides ui for network related testing.""" 4 5from __future__ import annotations 6 7import time 8import copy 9import weakref 10from threading import Thread 11from typing import TYPE_CHECKING 12 13from efro.error import CleanError 14import ba 15import ba.internal 16from bastd.ui.settings.testing import TestingWindow 17 18if TYPE_CHECKING: 19 from typing import Callable, Any 20 21# We generally want all net tests to timeout on their own, but we add 22# sort of sane max in case they don't. 23MAX_TEST_SECONDS = 60 * 2 24 25 26class NetTestingWindow(ba.Window): 27 """Window that runs a networking test suite to help diagnose issues.""" 28 29 def __init__(self, transition: str = 'in_right'): 30 self._width = 820 31 self._height = 500 32 self._printed_lines: list[str] = [] 33 uiscale = ba.app.ui.uiscale 34 super().__init__( 35 root_widget=ba.containerwidget( 36 size=(self._width, self._height), 37 scale=( 38 1.56 39 if uiscale is ba.UIScale.SMALL 40 else 1.2 41 if uiscale is ba.UIScale.MEDIUM 42 else 0.8 43 ), 44 stack_offset=(0.0, -7 if uiscale is ba.UIScale.SMALL else 0.0), 45 transition=transition, 46 ) 47 ) 48 self._done_button = ba.buttonwidget( 49 parent=self._root_widget, 50 position=(40, self._height - 77), 51 size=(120, 60), 52 scale=0.8, 53 autoselect=True, 54 label=ba.Lstr(resource='doneText'), 55 on_activate_call=self._done, 56 ) 57 58 self._copy_button = ba.buttonwidget( 59 parent=self._root_widget, 60 position=(self._width - 200, self._height - 77), 61 size=(100, 60), 62 scale=0.8, 63 autoselect=True, 64 label=ba.Lstr(resource='copyText'), 65 on_activate_call=self._copy, 66 ) 67 68 self._settings_button = ba.buttonwidget( 69 parent=self._root_widget, 70 position=(self._width - 100, self._height - 77), 71 size=(60, 60), 72 scale=0.8, 73 autoselect=True, 74 label=ba.Lstr(value='...'), 75 on_activate_call=self._show_val_testing, 76 ) 77 78 twidth = self._width - 450 79 ba.textwidget( 80 parent=self._root_widget, 81 position=(self._width * 0.5, self._height - 55), 82 size=(0, 0), 83 text=ba.Lstr(resource='settingsWindowAdvanced.netTestingText'), 84 color=(0.8, 0.8, 0.8, 1.0), 85 h_align='center', 86 v_align='center', 87 maxwidth=twidth, 88 ) 89 90 self._scroll = ba.scrollwidget( 91 parent=self._root_widget, 92 position=(50, 50), 93 size=(self._width - 100, self._height - 140), 94 capture_arrows=True, 95 autoselect=True, 96 ) 97 self._rows = ba.columnwidget(parent=self._scroll) 98 99 ba.containerwidget( 100 edit=self._root_widget, cancel_button=self._done_button 101 ) 102 103 # Now kick off the tests. 104 # Pass a weak-ref to this window so we don't keep it alive 105 # if we back out before it completes. Also set is as daemon 106 # so it doesn't keep the app running if the user is trying to quit. 107 Thread( 108 daemon=True, 109 target=ba.Call(_run_diagnostics, weakref.ref(self)), 110 ).start() 111 112 def print(self, text: str, color: tuple[float, float, float]) -> None: 113 """Print text to our console thingie.""" 114 for line in text.splitlines(): 115 txt = ba.textwidget( 116 parent=self._rows, 117 color=color, 118 text=line, 119 scale=0.75, 120 flatness=1.0, 121 shadow=0.0, 122 size=(0, 20), 123 ) 124 ba.containerwidget(edit=self._rows, visible_child=txt) 125 self._printed_lines.append(line) 126 127 def _copy(self) -> None: 128 if not ba.clipboard_is_supported(): 129 ba.screenmessage( 130 'Clipboard not supported on this platform.', color=(1, 0, 0) 131 ) 132 return 133 ba.clipboard_set_text('\n'.join(self._printed_lines)) 134 ba.screenmessage(f'{len(self._printed_lines)} lines copied.') 135 136 def _show_val_testing(self) -> None: 137 ba.app.ui.set_main_menu_window(NetValTestingWindow().get_root_widget()) 138 ba.containerwidget(edit=self._root_widget, transition='out_left') 139 140 def _done(self) -> None: 141 # pylint: disable=cyclic-import 142 from bastd.ui.settings.advanced import AdvancedSettingsWindow 143 144 ba.app.ui.set_main_menu_window( 145 AdvancedSettingsWindow(transition='in_left').get_root_widget() 146 ) 147 ba.containerwidget(edit=self._root_widget, transition='out_right') 148 149 150def _run_diagnostics(weakwin: weakref.ref[NetTestingWindow]) -> None: 151 # pylint: disable=too-many-statements 152 153 from efro.util import utc_now 154 155 have_error = [False] 156 157 # We're running in a background thread but UI stuff needs to run 158 # in the logic thread; give ourself a way to pass stuff to it. 159 def _print( 160 text: str, color: tuple[float, float, float] | None = None 161 ) -> None: 162 def _print_in_logic_thread() -> None: 163 win = weakwin() 164 if win is not None: 165 win.print(text, (1.0, 1.0, 1.0) if color is None else color) 166 167 ba.pushcall(_print_in_logic_thread, from_other_thread=True) 168 169 def _print_test_results(call: Callable[[], Any]) -> bool: 170 """Run the provided call, print result, & return success.""" 171 starttime = time.monotonic() 172 try: 173 call() 174 duration = time.monotonic() - starttime 175 _print(f'Succeeded in {duration:.2f}s.', color=(0, 1, 0)) 176 return True 177 except Exception as exc: 178 import traceback 179 180 duration = time.monotonic() - starttime 181 msg = ( 182 str(exc) 183 if isinstance(exc, CleanError) 184 else traceback.format_exc() 185 ) 186 _print(msg, color=(1.0, 1.0, 0.3)) 187 _print(f'Failed in {duration:.2f}s.', color=(1, 0, 0)) 188 have_error[0] = True 189 return False 190 191 try: 192 _print( 193 f'Running network diagnostics...\n' 194 f'ua: {ba.app.user_agent_string}\n' 195 f'time: {utc_now()}.' 196 ) 197 198 if bool(False): 199 _print('\nRunning dummy success test...') 200 _print_test_results(_dummy_success) 201 202 _print('\nRunning dummy fail test...') 203 _print_test_results(_dummy_fail) 204 205 # V1 ping 206 baseaddr = ba.internal.get_master_server_address(source=0, version=1) 207 _print(f'\nContacting V1 master-server src0 ({baseaddr})...') 208 v1worked = _print_test_results(lambda: _test_fetch(baseaddr)) 209 210 # V1 alternate ping (only if primary fails since this often fails). 211 if v1worked: 212 _print('\nSkipping V1 master-server src1 test since src0 worked.') 213 else: 214 baseaddr = ba.internal.get_master_server_address( 215 source=1, version=1 216 ) 217 _print(f'\nContacting V1 master-server src1 ({baseaddr})...') 218 _print_test_results(lambda: _test_fetch(baseaddr)) 219 220 if 'none succeeded' in ba.app.net.v1_test_log: 221 _print( 222 f'\nV1-test-log failed: {ba.app.net.v1_test_log}', 223 color=(1, 0, 0), 224 ) 225 have_error[0] = True 226 else: 227 _print(f'\nV1-test-log ok: {ba.app.net.v1_test_log}') 228 229 for srcid, result in sorted(ba.app.net.v1_ctest_results.items()): 230 _print(f'\nV1 src{srcid} result: {result}') 231 232 curv1addr = ba.internal.get_master_server_address(version=1) 233 _print(f'\nUsing V1 address: {curv1addr}') 234 235 _print('\nRunning V1 transaction...') 236 _print_test_results(_test_v1_transaction) 237 238 # V2 ping 239 baseaddr = ba.internal.get_master_server_address(version=2) 240 _print(f'\nContacting V2 master-server ({baseaddr})...') 241 _print_test_results(lambda: _test_fetch(baseaddr)) 242 243 _print('\nComparing local time to V2 server...') 244 _print_test_results(_test_v2_time) 245 246 # Get V2 nearby zone 247 with ba.app.net.zone_pings_lock: 248 zone_pings = copy.deepcopy(ba.app.net.zone_pings) 249 nearest_zone = ( 250 None 251 if not zone_pings 252 else sorted(zone_pings.items(), key=lambda i: i[1])[0] 253 ) 254 255 if nearest_zone is not None: 256 nearstr = f'{nearest_zone[0]}: {nearest_zone[1]:.0f}ms' 257 else: 258 nearstr = '-' 259 _print(f'\nChecking nearest V2 zone ping ({nearstr})...') 260 _print_test_results(lambda: _test_nearby_zone_ping(nearest_zone)) 261 262 _print('\nSending V2 cloud message...') 263 _print_test_results(_test_v2_cloud_message) 264 265 if have_error[0]: 266 _print( 267 '\nDiagnostics complete. Some diagnostics failed.', 268 color=(10, 0, 0), 269 ) 270 else: 271 _print( 272 '\nDiagnostics complete. Everything looks good!', 273 color=(0, 1, 0), 274 ) 275 except Exception: 276 import traceback 277 278 _print( 279 f'An unexpected error occurred during testing;' 280 f' please report this.\n' 281 f'{traceback.format_exc()}', 282 color=(1, 0, 0), 283 ) 284 285 286def _dummy_success() -> None: 287 """Dummy success test.""" 288 time.sleep(1.2) 289 290 291def _dummy_fail() -> None: 292 """Dummy fail test case.""" 293 raise RuntimeError('fail-test') 294 295 296def _test_v1_transaction() -> None: 297 """Dummy fail test case.""" 298 if ba.internal.get_v1_account_state() != 'signed_in': 299 raise RuntimeError('Not signed in.') 300 301 starttime = time.monotonic() 302 303 # Gets set to True on success or string on error. 304 results: list[Any] = [False] 305 306 def _cb(cbresults: Any) -> None: 307 # Simply set results here; our other thread acts on them. 308 if not isinstance(cbresults, dict) or 'party_code' not in cbresults: 309 results[0] = 'Unexpected transaction response' 310 return 311 results[0] = True # Success! 312 313 def _do_it() -> None: 314 # Fire off a transaction with a callback. 315 ba.internal.add_transaction( 316 { 317 'type': 'PRIVATE_PARTY_QUERY', 318 'expire_time': time.time() + 20, 319 }, 320 callback=_cb, 321 ) 322 ba.internal.run_transactions() 323 324 ba.pushcall(_do_it, from_other_thread=True) 325 326 while results[0] is False: 327 time.sleep(0.01) 328 if time.monotonic() - starttime > MAX_TEST_SECONDS: 329 raise RuntimeError( 330 f'test timed out after {MAX_TEST_SECONDS} seconds' 331 ) 332 333 # If we got left a string, its an error. 334 if isinstance(results[0], str): 335 raise RuntimeError(results[0]) 336 337 338def _test_v2_cloud_message() -> None: 339 from dataclasses import dataclass 340 import bacommon.cloud 341 342 @dataclass 343 class _Results: 344 errstr: str | None = None 345 send_time: float | None = None 346 response_time: float | None = None 347 348 results = _Results() 349 350 def _cb(response: bacommon.cloud.PingResponse | Exception) -> None: 351 # Note: this runs in another thread so need to avoid exceptions. 352 results.response_time = time.monotonic() 353 if isinstance(response, Exception): 354 results.errstr = str(response) 355 if not isinstance(response, bacommon.cloud.PingResponse): 356 results.errstr = f'invalid response type: {type(response)}.' 357 358 def _send() -> None: 359 # Note: this runs in another thread so need to avoid exceptions. 360 results.send_time = time.monotonic() 361 ba.app.cloud.send_message_cb(bacommon.cloud.PingMessage(), _cb) 362 363 # This stuff expects to be run from the logic thread. 364 ba.pushcall(_send, from_other_thread=True) 365 366 wait_start_time = time.monotonic() 367 while True: 368 if results.response_time is not None: 369 break 370 time.sleep(0.01) 371 if time.monotonic() - wait_start_time > MAX_TEST_SECONDS: 372 raise RuntimeError( 373 f'Timeout ({MAX_TEST_SECONDS} seconds)' 374 f' waiting for cloud message response' 375 ) 376 if results.errstr is not None: 377 raise RuntimeError(results.errstr) 378 379 380def _test_v2_time() -> None: 381 offset = ba.app.net.server_time_offset_hours 382 if offset is None: 383 raise RuntimeError( 384 'no time offset found;' 385 ' perhaps unable to communicate with v2 server?' 386 ) 387 if abs(offset) >= 2.0: 388 raise CleanError( 389 f'Your device time is off from world time by {offset:.1f} hours.\n' 390 'This may cause network operations to fail due to your device\n' 391 ' incorrectly treating SSL certificates as not-yet-valid, etc.\n' 392 'Check your device time and time-zone settings to fix this.\n' 393 ) 394 395 396def _test_fetch(baseaddr: str) -> None: 397 # pylint: disable=consider-using-with 398 import urllib.request 399 400 response = urllib.request.urlopen( 401 urllib.request.Request( 402 f'{baseaddr}/ping', None, {'User-Agent': ba.app.user_agent_string} 403 ), 404 context=ba.app.net.sslcontext, 405 timeout=MAX_TEST_SECONDS, 406 ) 407 if response.getcode() != 200: 408 raise RuntimeError( 409 f'Got unexpected response code {response.getcode()}.' 410 ) 411 data = response.read() 412 if data != b'pong': 413 raise RuntimeError('Got unexpected response data.') 414 415 416def _test_nearby_zone_ping(nearest_zone: tuple[str, float] | None) -> None: 417 """Try to ping nearest v2 zone.""" 418 if nearest_zone is None: 419 raise RuntimeError('No nearest zone.') 420 if nearest_zone[1] > 500: 421 raise RuntimeError('Ping too high.') 422 423 424class NetValTestingWindow(TestingWindow): 425 """Window to test network related settings.""" 426 427 def __init__(self, transition: str = 'in_right'): 428 429 entries = [ 430 {'name': 'bufferTime', 'label': 'Buffer Time', 'increment': 1.0}, 431 { 432 'name': 'delaySampling', 433 'label': 'Delay Sampling', 434 'increment': 1.0, 435 }, 436 { 437 'name': 'dynamicsSyncTime', 438 'label': 'Dynamics Sync Time', 439 'increment': 10, 440 }, 441 {'name': 'showNetInfo', 'label': 'Show Net Info', 'increment': 1}, 442 ] 443 super().__init__( 444 title=ba.Lstr(resource='settingsWindowAdvanced.netTestingText'), 445 entries=entries, 446 transition=transition, 447 back_call=lambda: NetTestingWindow(transition='in_left'), 448 )
class
NetTestingWindow(ba.ui.Window):
27class NetTestingWindow(ba.Window): 28 """Window that runs a networking test suite to help diagnose issues.""" 29 30 def __init__(self, transition: str = 'in_right'): 31 self._width = 820 32 self._height = 500 33 self._printed_lines: list[str] = [] 34 uiscale = ba.app.ui.uiscale 35 super().__init__( 36 root_widget=ba.containerwidget( 37 size=(self._width, self._height), 38 scale=( 39 1.56 40 if uiscale is ba.UIScale.SMALL 41 else 1.2 42 if uiscale is ba.UIScale.MEDIUM 43 else 0.8 44 ), 45 stack_offset=(0.0, -7 if uiscale is ba.UIScale.SMALL else 0.0), 46 transition=transition, 47 ) 48 ) 49 self._done_button = ba.buttonwidget( 50 parent=self._root_widget, 51 position=(40, self._height - 77), 52 size=(120, 60), 53 scale=0.8, 54 autoselect=True, 55 label=ba.Lstr(resource='doneText'), 56 on_activate_call=self._done, 57 ) 58 59 self._copy_button = ba.buttonwidget( 60 parent=self._root_widget, 61 position=(self._width - 200, self._height - 77), 62 size=(100, 60), 63 scale=0.8, 64 autoselect=True, 65 label=ba.Lstr(resource='copyText'), 66 on_activate_call=self._copy, 67 ) 68 69 self._settings_button = ba.buttonwidget( 70 parent=self._root_widget, 71 position=(self._width - 100, self._height - 77), 72 size=(60, 60), 73 scale=0.8, 74 autoselect=True, 75 label=ba.Lstr(value='...'), 76 on_activate_call=self._show_val_testing, 77 ) 78 79 twidth = self._width - 450 80 ba.textwidget( 81 parent=self._root_widget, 82 position=(self._width * 0.5, self._height - 55), 83 size=(0, 0), 84 text=ba.Lstr(resource='settingsWindowAdvanced.netTestingText'), 85 color=(0.8, 0.8, 0.8, 1.0), 86 h_align='center', 87 v_align='center', 88 maxwidth=twidth, 89 ) 90 91 self._scroll = ba.scrollwidget( 92 parent=self._root_widget, 93 position=(50, 50), 94 size=(self._width - 100, self._height - 140), 95 capture_arrows=True, 96 autoselect=True, 97 ) 98 self._rows = ba.columnwidget(parent=self._scroll) 99 100 ba.containerwidget( 101 edit=self._root_widget, cancel_button=self._done_button 102 ) 103 104 # Now kick off the tests. 105 # Pass a weak-ref to this window so we don't keep it alive 106 # if we back out before it completes. Also set is as daemon 107 # so it doesn't keep the app running if the user is trying to quit. 108 Thread( 109 daemon=True, 110 target=ba.Call(_run_diagnostics, weakref.ref(self)), 111 ).start() 112 113 def print(self, text: str, color: tuple[float, float, float]) -> None: 114 """Print text to our console thingie.""" 115 for line in text.splitlines(): 116 txt = ba.textwidget( 117 parent=self._rows, 118 color=color, 119 text=line, 120 scale=0.75, 121 flatness=1.0, 122 shadow=0.0, 123 size=(0, 20), 124 ) 125 ba.containerwidget(edit=self._rows, visible_child=txt) 126 self._printed_lines.append(line) 127 128 def _copy(self) -> None: 129 if not ba.clipboard_is_supported(): 130 ba.screenmessage( 131 'Clipboard not supported on this platform.', color=(1, 0, 0) 132 ) 133 return 134 ba.clipboard_set_text('\n'.join(self._printed_lines)) 135 ba.screenmessage(f'{len(self._printed_lines)} lines copied.') 136 137 def _show_val_testing(self) -> None: 138 ba.app.ui.set_main_menu_window(NetValTestingWindow().get_root_widget()) 139 ba.containerwidget(edit=self._root_widget, transition='out_left') 140 141 def _done(self) -> None: 142 # pylint: disable=cyclic-import 143 from bastd.ui.settings.advanced import AdvancedSettingsWindow 144 145 ba.app.ui.set_main_menu_window( 146 AdvancedSettingsWindow(transition='in_left').get_root_widget() 147 ) 148 ba.containerwidget(edit=self._root_widget, transition='out_right')
Window that runs a networking test suite to help diagnose issues.
NetTestingWindow(transition: str = 'in_right')
30 def __init__(self, transition: str = 'in_right'): 31 self._width = 820 32 self._height = 500 33 self._printed_lines: list[str] = [] 34 uiscale = ba.app.ui.uiscale 35 super().__init__( 36 root_widget=ba.containerwidget( 37 size=(self._width, self._height), 38 scale=( 39 1.56 40 if uiscale is ba.UIScale.SMALL 41 else 1.2 42 if uiscale is ba.UIScale.MEDIUM 43 else 0.8 44 ), 45 stack_offset=(0.0, -7 if uiscale is ba.UIScale.SMALL else 0.0), 46 transition=transition, 47 ) 48 ) 49 self._done_button = ba.buttonwidget( 50 parent=self._root_widget, 51 position=(40, self._height - 77), 52 size=(120, 60), 53 scale=0.8, 54 autoselect=True, 55 label=ba.Lstr(resource='doneText'), 56 on_activate_call=self._done, 57 ) 58 59 self._copy_button = ba.buttonwidget( 60 parent=self._root_widget, 61 position=(self._width - 200, self._height - 77), 62 size=(100, 60), 63 scale=0.8, 64 autoselect=True, 65 label=ba.Lstr(resource='copyText'), 66 on_activate_call=self._copy, 67 ) 68 69 self._settings_button = ba.buttonwidget( 70 parent=self._root_widget, 71 position=(self._width - 100, self._height - 77), 72 size=(60, 60), 73 scale=0.8, 74 autoselect=True, 75 label=ba.Lstr(value='...'), 76 on_activate_call=self._show_val_testing, 77 ) 78 79 twidth = self._width - 450 80 ba.textwidget( 81 parent=self._root_widget, 82 position=(self._width * 0.5, self._height - 55), 83 size=(0, 0), 84 text=ba.Lstr(resource='settingsWindowAdvanced.netTestingText'), 85 color=(0.8, 0.8, 0.8, 1.0), 86 h_align='center', 87 v_align='center', 88 maxwidth=twidth, 89 ) 90 91 self._scroll = ba.scrollwidget( 92 parent=self._root_widget, 93 position=(50, 50), 94 size=(self._width - 100, self._height - 140), 95 capture_arrows=True, 96 autoselect=True, 97 ) 98 self._rows = ba.columnwidget(parent=self._scroll) 99 100 ba.containerwidget( 101 edit=self._root_widget, cancel_button=self._done_button 102 ) 103 104 # Now kick off the tests. 105 # Pass a weak-ref to this window so we don't keep it alive 106 # if we back out before it completes. Also set is as daemon 107 # so it doesn't keep the app running if the user is trying to quit. 108 Thread( 109 daemon=True, 110 target=ba.Call(_run_diagnostics, weakref.ref(self)), 111 ).start()
def
print(self, text: str, color: tuple[float, float, float]) -> None:
113 def print(self, text: str, color: tuple[float, float, float]) -> None: 114 """Print text to our console thingie.""" 115 for line in text.splitlines(): 116 txt = ba.textwidget( 117 parent=self._rows, 118 color=color, 119 text=line, 120 scale=0.75, 121 flatness=1.0, 122 shadow=0.0, 123 size=(0, 20), 124 ) 125 ba.containerwidget(edit=self._rows, visible_child=txt) 126 self._printed_lines.append(line)
Print text to our console thingie.
Inherited Members
- ba.ui.Window
- get_root_widget
425class NetValTestingWindow(TestingWindow): 426 """Window to test network related settings.""" 427 428 def __init__(self, transition: str = 'in_right'): 429 430 entries = [ 431 {'name': 'bufferTime', 'label': 'Buffer Time', 'increment': 1.0}, 432 { 433 'name': 'delaySampling', 434 'label': 'Delay Sampling', 435 'increment': 1.0, 436 }, 437 { 438 'name': 'dynamicsSyncTime', 439 'label': 'Dynamics Sync Time', 440 'increment': 10, 441 }, 442 {'name': 'showNetInfo', 'label': 'Show Net Info', 'increment': 1}, 443 ] 444 super().__init__( 445 title=ba.Lstr(resource='settingsWindowAdvanced.netTestingText'), 446 entries=entries, 447 transition=transition, 448 back_call=lambda: NetTestingWindow(transition='in_left'), 449 )
Window to test network related settings.
NetValTestingWindow(transition: str = 'in_right')
428 def __init__(self, transition: str = 'in_right'): 429 430 entries = [ 431 {'name': 'bufferTime', 'label': 'Buffer Time', 'increment': 1.0}, 432 { 433 'name': 'delaySampling', 434 'label': 'Delay Sampling', 435 'increment': 1.0, 436 }, 437 { 438 'name': 'dynamicsSyncTime', 439 'label': 'Dynamics Sync Time', 440 'increment': 10, 441 }, 442 {'name': 'showNetInfo', 'label': 'Show Net Info', 'increment': 1}, 443 ] 444 super().__init__( 445 title=ba.Lstr(resource='settingsWindowAdvanced.netTestingText'), 446 entries=entries, 447 transition=transition, 448 back_call=lambda: NetTestingWindow(transition='in_left'), 449 )
Inherited Members
- ba.ui.Window
- get_root_widget