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

Print text to our console thingie.

Inherited Members
ba.ui.Window
get_root_widget
class NetValTestingWindow(bastd.ui.settings.testing.TestingWindow):
361class NetValTestingWindow(TestingWindow):
362    """Window to test network related settings."""
363
364    def __init__(self, transition: str = 'in_right'):
365
366        entries = [
367            {
368                'name': 'bufferTime',
369                'label': 'Buffer Time',
370                'increment': 1.0
371            },
372            {
373                'name': 'delaySampling',
374                'label': 'Delay Sampling',
375                'increment': 1.0
376            },
377            {
378                'name': 'dynamicsSyncTime',
379                'label': 'Dynamics Sync Time',
380                'increment': 10
381            },
382            {
383                'name': 'showNetInfo',
384                'label': 'Show Net Info',
385                'increment': 1
386            },
387        ]
388        super().__init__(
389            title=ba.Lstr(resource='settingsWindowAdvanced.netTestingText'),
390            entries=entries,
391            transition=transition,
392            back_call=lambda: NetTestingWindow(transition='in_left'))

Window to test network related settings.

NetValTestingWindow(transition: str = 'in_right')
364    def __init__(self, transition: str = 'in_right'):
365
366        entries = [
367            {
368                'name': 'bufferTime',
369                'label': 'Buffer Time',
370                'increment': 1.0
371            },
372            {
373                'name': 'delaySampling',
374                'label': 'Delay Sampling',
375                'increment': 1.0
376            },
377            {
378                'name': 'dynamicsSyncTime',
379                'label': 'Dynamics Sync Time',
380                'increment': 10
381            },
382            {
383                'name': 'showNetInfo',
384                'label': 'Show Net Info',
385                'increment': 1
386            },
387        ]
388        super().__init__(
389            title=ba.Lstr(resource='settingsWindowAdvanced.netTestingText'),
390            entries=entries,
391            transition=transition,
392            back_call=lambda: NetTestingWindow(transition='in_left'))
Inherited Members
ba.ui.Window
get_root_widget