mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-01-05 13:01:12 +00:00
feat(all): increase stability of debug-decision events + parsing JSON from Rust
This commit is contained in:
parent
c6ad596339
commit
bd6783b1a3
@ -5,6 +5,7 @@ if not __debug__:
|
|||||||
|
|
||||||
if __debug__:
|
if __debug__:
|
||||||
from storage import debug as storage
|
from storage import debug as storage
|
||||||
|
from storage.debug import debug_events
|
||||||
|
|
||||||
import trezorui2
|
import trezorui2
|
||||||
|
|
||||||
@ -30,17 +31,17 @@ if __debug__:
|
|||||||
DebugLinkReseedRandom,
|
DebugLinkReseedRandom,
|
||||||
DebugLinkState,
|
DebugLinkState,
|
||||||
DebugLinkWatchLayout,
|
DebugLinkWatchLayout,
|
||||||
|
DebugLinkResetDebugEvents,
|
||||||
)
|
)
|
||||||
|
|
||||||
reset_current_words = loop.chan()
|
|
||||||
reset_word_index = loop.chan()
|
|
||||||
|
|
||||||
confirm_chan = loop.chan()
|
|
||||||
swipe_chan = loop.chan()
|
swipe_chan = loop.chan()
|
||||||
input_chan = loop.chan()
|
result_chan = loop.chan()
|
||||||
confirm_signal = confirm_chan.take
|
button_chan = loop.chan()
|
||||||
|
click_chan = loop.chan()
|
||||||
swipe_signal = swipe_chan.take
|
swipe_signal = swipe_chan.take
|
||||||
input_signal = input_chan.take
|
result_signal = result_chan.take
|
||||||
|
button_signal = button_chan.take
|
||||||
|
click_signal = click_chan.take
|
||||||
|
|
||||||
debuglink_decision_chan = loop.chan()
|
debuglink_decision_chan = loop.chan()
|
||||||
|
|
||||||
@ -64,65 +65,80 @@ if __debug__:
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def notify_layout_change(layout: Layout) -> None:
|
def notify_layout_change(layout: Layout, event_id: int | None = None) -> None:
|
||||||
storage.current_content[:] = layout.read_content()
|
layout.read_content_into(storage.current_content_tokens)
|
||||||
if storage.watch_layout_changes or layout_change_chan.takers:
|
if storage.watch_layout_changes or layout_change_chan.takers:
|
||||||
layout_change_chan.publish(storage.current_content)
|
payload = (event_id, storage.current_content_tokens)
|
||||||
|
layout_change_chan.publish(payload)
|
||||||
|
|
||||||
async def _dispatch_debuglink_decision(msg: DebugLinkDecision) -> None:
|
async def _dispatch_debuglink_decision(
|
||||||
from trezor.enums import DebugButton, DebugSwipeDirection
|
event_id: int | None, msg: DebugLinkDecision
|
||||||
from trezor.ui import (
|
) -> None:
|
||||||
Result,
|
from trezor.enums import DebugButton
|
||||||
SWIPE_UP,
|
|
||||||
SWIPE_DOWN,
|
|
||||||
SWIPE_LEFT,
|
|
||||||
SWIPE_RIGHT,
|
|
||||||
)
|
|
||||||
|
|
||||||
button = msg.button # local_cache_attribute
|
if msg.button is not None:
|
||||||
swipe = msg.swipe # local_cache_attribute
|
if msg.button == DebugButton.NO:
|
||||||
|
await result_chan.put((event_id, trezorui2.CANCELLED))
|
||||||
if button is not None:
|
elif msg.button == DebugButton.YES:
|
||||||
if button == DebugButton.NO:
|
await result_chan.put((event_id, trezorui2.CONFIRMED))
|
||||||
await confirm_chan.put(Result(trezorui2.CANCELLED))
|
elif msg.button == DebugButton.INFO:
|
||||||
elif button == DebugButton.YES:
|
await result_chan.put((event_id, trezorui2.INFO))
|
||||||
await confirm_chan.put(Result(trezorui2.CONFIRMED))
|
else:
|
||||||
elif button == DebugButton.INFO:
|
raise RuntimeError(f"Invalid msg.button - {msg.button}")
|
||||||
await confirm_chan.put(Result(trezorui2.INFO))
|
elif msg.input is not None:
|
||||||
if swipe is not None:
|
await result_chan.put((event_id, msg.input))
|
||||||
if swipe == DebugSwipeDirection.UP:
|
elif msg.swipe is not None:
|
||||||
await swipe_chan.put(SWIPE_UP)
|
await swipe_chan.put((event_id, msg.swipe))
|
||||||
elif swipe == DebugSwipeDirection.DOWN:
|
else:
|
||||||
await swipe_chan.put(SWIPE_DOWN)
|
# Sanity check. The message will be visible in terminal.
|
||||||
elif swipe == DebugSwipeDirection.LEFT:
|
raise RuntimeError("Invalid DebugLinkDecision message")
|
||||||
await swipe_chan.put(SWIPE_LEFT)
|
|
||||||
elif swipe == DebugSwipeDirection.RIGHT:
|
|
||||||
await swipe_chan.put(SWIPE_RIGHT)
|
|
||||||
if msg.input is not None:
|
|
||||||
await input_chan.put(Result(msg.input))
|
|
||||||
|
|
||||||
async def debuglink_decision_dispatcher() -> None:
|
async def debuglink_decision_dispatcher() -> None:
|
||||||
while True:
|
while True:
|
||||||
msg = await debuglink_decision_chan.take()
|
event_id, msg = await debuglink_decision_chan.take()
|
||||||
await _dispatch_debuglink_decision(msg)
|
await _dispatch_debuglink_decision(event_id, msg)
|
||||||
|
|
||||||
|
async def get_layout_change_content() -> list[str]:
|
||||||
|
awaited_event_id = debug_events.awaited_event
|
||||||
|
last_result_id = debug_events.last_result
|
||||||
|
|
||||||
|
if awaited_event_id is not None and awaited_event_id == last_result_id:
|
||||||
|
# We are awaiting the event that just happened - return current state
|
||||||
|
return storage.current_content_tokens
|
||||||
|
|
||||||
|
while True:
|
||||||
|
event_id, content = await layout_change_chan.take()
|
||||||
|
if awaited_event_id is None or event_id is None:
|
||||||
|
# Not waiting for anything or event does not have ID
|
||||||
|
break
|
||||||
|
elif event_id == awaited_event_id:
|
||||||
|
# We found what we were waiting for
|
||||||
|
debug_events.awaited_event = None
|
||||||
|
break
|
||||||
|
elif event_id > awaited_event_id:
|
||||||
|
# Sanity check
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Waiting for event that already happened - {event_id} > {awaited_event_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if awaited_event_id is not None:
|
||||||
|
# Updating last result
|
||||||
|
debug_events.last_result = awaited_event_id
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
async def return_layout_change() -> None:
|
async def return_layout_change() -> None:
|
||||||
content = await layout_change_chan.take()
|
content_tokens = await get_layout_change_content()
|
||||||
|
|
||||||
assert DEBUG_CONTEXT is not None
|
assert DEBUG_CONTEXT is not None
|
||||||
if storage.layout_watcher is LAYOUT_WATCHER_LAYOUT:
|
if storage.layout_watcher is LAYOUT_WATCHER_LAYOUT:
|
||||||
await DEBUG_CONTEXT.write(DebugLinkLayout(lines=content))
|
await DEBUG_CONTEXT.write(DebugLinkLayout(tokens=content_tokens))
|
||||||
else:
|
else:
|
||||||
from trezor.messages import DebugLinkState
|
from trezor.messages import DebugLinkState
|
||||||
|
|
||||||
await DEBUG_CONTEXT.write(DebugLinkState(layout_lines=content))
|
await DEBUG_CONTEXT.write(DebugLinkState(tokens=content_tokens))
|
||||||
storage.layout_watcher = LAYOUT_WATCHER_NONE
|
storage.layout_watcher = LAYOUT_WATCHER_NONE
|
||||||
|
|
||||||
async def touch_hold(x: int, y: int, duration_ms: int) -> None:
|
|
||||||
from trezor import io
|
|
||||||
|
|
||||||
await loop.sleep(duration_ms)
|
|
||||||
loop.synthetic_events.append((io.TOUCH, (io.TOUCH_END, x, y)))
|
|
||||||
|
|
||||||
async def dispatch_DebugLinkWatchLayout(
|
async def dispatch_DebugLinkWatchLayout(
|
||||||
ctx: wire.Context, msg: DebugLinkWatchLayout
|
ctx: wire.Context, msg: DebugLinkWatchLayout
|
||||||
) -> Success:
|
) -> Success:
|
||||||
@ -135,30 +151,40 @@ if __debug__:
|
|||||||
log.debug(__name__, "Watch layout changes: %s", storage.watch_layout_changes)
|
log.debug(__name__, "Watch layout changes: %s", storage.watch_layout_changes)
|
||||||
return Success()
|
return Success()
|
||||||
|
|
||||||
|
async def dispatch_DebugLinkResetDebugEvents(
|
||||||
|
ctx: wire.Context, msg: DebugLinkResetDebugEvents
|
||||||
|
) -> Success:
|
||||||
|
# Resetting the debug events makes sure that the previous
|
||||||
|
# events/layouts are not mixed with the new ones.
|
||||||
|
storage.reset_debug_events()
|
||||||
|
return Success()
|
||||||
|
|
||||||
async def dispatch_DebugLinkDecision(
|
async def dispatch_DebugLinkDecision(
|
||||||
ctx: wire.Context, msg: DebugLinkDecision
|
ctx: wire.Context, msg: DebugLinkDecision
|
||||||
) -> None:
|
) -> None:
|
||||||
from trezor import io, workflow
|
from trezor import workflow
|
||||||
|
|
||||||
workflow.idle_timer.touch()
|
workflow.idle_timer.touch()
|
||||||
|
|
||||||
if debuglink_decision_chan.putters:
|
if debuglink_decision_chan.putters:
|
||||||
log.warning(__name__, "DebugLinkDecision queue is not empty")
|
log.warning(__name__, "DebugLinkDecision queue is not empty")
|
||||||
|
|
||||||
x = msg.x # local_cache_attribute
|
x = msg.x # local_cache_attribute
|
||||||
y = msg.y # local_cache_attribute
|
y = msg.y # local_cache_attribute
|
||||||
|
|
||||||
|
# Incrementing the counter for last events so we know what to await
|
||||||
|
debug_events.last_event += 1
|
||||||
|
|
||||||
|
# TT click on specific coordinates, with possible hold
|
||||||
if x is not None and y is not None:
|
if x is not None and y is not None:
|
||||||
evt_down = io.TOUCH_START, x, y
|
click_chan.publish((debug_events.last_event, x, y, msg.hold_ms))
|
||||||
evt_up = io.TOUCH_END, x, y
|
|
||||||
loop.synthetic_events.append((io.TOUCH, evt_down))
|
|
||||||
if msg.hold_ms is not None:
|
|
||||||
loop.schedule(touch_hold(x, y, msg.hold_ms))
|
|
||||||
else:
|
else:
|
||||||
loop.synthetic_events.append((io.TOUCH, evt_up))
|
# Will get picked up by _dispatch_debuglink_decision eventually
|
||||||
else:
|
debuglink_decision_chan.publish((debug_events.last_event, msg))
|
||||||
debuglink_decision_chan.publish(msg)
|
|
||||||
|
|
||||||
if msg.wait:
|
if msg.wait:
|
||||||
|
# We wait for all the previously sent events
|
||||||
|
debug_events.awaited_event = debug_events.last_event
|
||||||
storage.layout_watcher = LAYOUT_WATCHER_LAYOUT
|
storage.layout_watcher = LAYOUT_WATCHER_LAYOUT
|
||||||
loop.schedule(return_layout_change())
|
loop.schedule(return_layout_change())
|
||||||
|
|
||||||
@ -178,15 +204,13 @@ if __debug__:
|
|||||||
if not storage.watch_layout_changes:
|
if not storage.watch_layout_changes:
|
||||||
raise wire.ProcessError("Layout is not watched")
|
raise wire.ProcessError("Layout is not watched")
|
||||||
storage.layout_watcher = LAYOUT_WATCHER_STATE
|
storage.layout_watcher = LAYOUT_WATCHER_STATE
|
||||||
|
# We wait for the last previously sent event to finish
|
||||||
|
debug_events.awaited_event = debug_events.last_event
|
||||||
loop.schedule(return_layout_change())
|
loop.schedule(return_layout_change())
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
m.layout_lines = storage.current_content
|
m.tokens = storage.current_content_tokens
|
||||||
|
|
||||||
if msg.wait_word_pos:
|
|
||||||
m.reset_word_pos = await reset_word_index.take()
|
|
||||||
if msg.wait_word_list:
|
|
||||||
m.reset_word = " ".join(await reset_current_words.take())
|
|
||||||
return m
|
return m
|
||||||
|
|
||||||
async def dispatch_DebugLinkRecordScreen(
|
async def dispatch_DebugLinkRecordScreen(
|
||||||
@ -248,6 +272,9 @@ if __debug__:
|
|||||||
register(MessageType.DebugLinkRecordScreen, dispatch_DebugLinkRecordScreen)
|
register(MessageType.DebugLinkRecordScreen, dispatch_DebugLinkRecordScreen)
|
||||||
register(MessageType.DebugLinkEraseSdCard, dispatch_DebugLinkEraseSdCard)
|
register(MessageType.DebugLinkEraseSdCard, dispatch_DebugLinkEraseSdCard)
|
||||||
register(MessageType.DebugLinkWatchLayout, dispatch_DebugLinkWatchLayout)
|
register(MessageType.DebugLinkWatchLayout, dispatch_DebugLinkWatchLayout)
|
||||||
|
register(
|
||||||
|
MessageType.DebugLinkResetDebugEvents, dispatch_DebugLinkResetDebugEvents
|
||||||
|
)
|
||||||
|
|
||||||
loop.schedule(debuglink_decision_dispatcher())
|
loop.schedule(debuglink_decision_dispatcher())
|
||||||
if storage.layout_watcher is not LAYOUT_WATCHER_NONE:
|
if storage.layout_watcher is not LAYOUT_WATCHER_NONE:
|
||||||
|
@ -16,9 +16,6 @@ if TYPE_CHECKING:
|
|||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
from trezor.wire import GenericContext
|
from trezor.wire import GenericContext
|
||||||
|
|
||||||
if __debug__:
|
|
||||||
from apps import debug
|
|
||||||
|
|
||||||
_NUM_OF_CHOICES = const(3)
|
_NUM_OF_CHOICES = const(3)
|
||||||
|
|
||||||
|
|
||||||
@ -57,10 +54,6 @@ async def _confirm_word(
|
|||||||
checked_index = share_words.index(checked_word) + offset
|
checked_index = share_words.index(checked_word) + offset
|
||||||
# shuffle again so the confirmed word is not always the first choice
|
# shuffle again so the confirmed word is not always the first choice
|
||||||
random.shuffle(choices)
|
random.shuffle(choices)
|
||||||
|
|
||||||
if __debug__:
|
|
||||||
debug.reset_word_index.publish(checked_index)
|
|
||||||
|
|
||||||
# let the user pick a word
|
# let the user pick a word
|
||||||
selected_word: str = await select_word(
|
selected_word: str = await select_word(
|
||||||
ctx, choices, share_index, checked_index, count, group_index
|
ctx, choices, share_index, checked_index, count, group_index
|
||||||
|
@ -7,10 +7,28 @@ if __debug__:
|
|||||||
save_screen = False
|
save_screen = False
|
||||||
save_screen_directory = "."
|
save_screen_directory = "."
|
||||||
|
|
||||||
current_content: list[str] = [""] * 20
|
current_content_tokens: list[str] = [""] * 60
|
||||||
current_content.clear()
|
current_content_tokens.clear()
|
||||||
|
|
||||||
watch_layout_changes = False
|
watch_layout_changes = False
|
||||||
layout_watcher = 0
|
layout_watcher = 0
|
||||||
|
|
||||||
reset_internal_entropy: bytes = b""
|
reset_internal_entropy: bytes = b""
|
||||||
|
|
||||||
|
class DebugEvents:
|
||||||
|
def __init__(self):
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
self.last_event = 0
|
||||||
|
self.last_result: int | None = None
|
||||||
|
self.awaited_event: int | None = None
|
||||||
|
|
||||||
|
debug_events = DebugEvents()
|
||||||
|
|
||||||
|
def reset_debug_events() -> None:
|
||||||
|
debug_events.reset()
|
||||||
|
|
||||||
|
# Event resulted in the layout change, call
|
||||||
|
# notify_layout_change with this ID in first_paint of next layout.
|
||||||
|
new_layout_event_id: int | None = None
|
||||||
|
@ -41,10 +41,6 @@ _finalizers: dict[int, Finalizer] = {}
|
|||||||
# reference to the task that is currently executing
|
# reference to the task that is currently executing
|
||||||
this_task: Task | None = None
|
this_task: Task | None = None
|
||||||
|
|
||||||
if __debug__:
|
|
||||||
# synthetic event queue
|
|
||||||
synthetic_events: list[tuple[int, Any]] = []
|
|
||||||
|
|
||||||
|
|
||||||
class TaskClosed(Exception):
|
class TaskClosed(Exception):
|
||||||
pass
|
pass
|
||||||
@ -118,20 +114,6 @@ def run() -> None:
|
|||||||
task_entry = [0, 0, 0] # deadline, task, value
|
task_entry = [0, 0, 0] # deadline, task, value
|
||||||
msg_entry = [0, 0] # iface | flags, value
|
msg_entry = [0, 0] # iface | flags, value
|
||||||
while _queue or _paused:
|
while _queue or _paused:
|
||||||
if __debug__:
|
|
||||||
# process synthetic events
|
|
||||||
if synthetic_events:
|
|
||||||
iface, event = synthetic_events[0]
|
|
||||||
msg_tasks = _paused.pop(iface, ())
|
|
||||||
if msg_tasks:
|
|
||||||
synthetic_events.pop(0)
|
|
||||||
for task in msg_tasks:
|
|
||||||
_step(task, event)
|
|
||||||
|
|
||||||
# XXX: we assume that synthetic events are rare. If there is a lot of them,
|
|
||||||
# this degrades to "while synthetic_events" and would ignore all real ones.
|
|
||||||
continue
|
|
||||||
|
|
||||||
# compute the maximum amount of time we can wait for a message
|
# compute the maximum amount of time we can wait for a message
|
||||||
if _queue:
|
if _queue:
|
||||||
delay = utime.ticks_diff(_queue.peektime(), utime.ticks_ms())
|
delay = utime.ticks_diff(_queue.peektime(), utime.ticks_ms())
|
||||||
|
@ -17,14 +17,6 @@ MONO: int = Display.FONT_MONO
|
|||||||
WIDTH: int = Display.WIDTH
|
WIDTH: int = Display.WIDTH
|
||||||
HEIGHT: int = Display.HEIGHT
|
HEIGHT: int = Display.HEIGHT
|
||||||
|
|
||||||
if __debug__:
|
|
||||||
# common symbols to transfer swipes between debuglink and the UI
|
|
||||||
SWIPE_UP = const(0x01)
|
|
||||||
SWIPE_DOWN = const(0x02)
|
|
||||||
SWIPE_LEFT = const(0x04)
|
|
||||||
SWIPE_RIGHT = const(0x08)
|
|
||||||
|
|
||||||
|
|
||||||
# channel used to cancel layouts, see `Cancelled` exception
|
# channel used to cancel layouts, see `Cancelled` exception
|
||||||
layout_chan = loop.chan()
|
layout_chan = loop.chan()
|
||||||
|
|
||||||
@ -172,8 +164,9 @@ class Component:
|
|||||||
|
|
||||||
if __debug__:
|
if __debug__:
|
||||||
|
|
||||||
def read_content(self) -> list[str]:
|
def read_content_into(self, content_store: list[str]) -> None:
|
||||||
return [self.__class__.__name__]
|
content_store.clear()
|
||||||
|
content_store.append(self.__class__.__name__)
|
||||||
|
|
||||||
|
|
||||||
class Result(Exception):
|
class Result(Exception):
|
||||||
|
@ -32,14 +32,10 @@ if __debug__:
|
|||||||
|
|
||||||
class RustLayout(ui.Layout):
|
class RustLayout(ui.Layout):
|
||||||
# pylint: disable=super-init-not-called
|
# pylint: disable=super-init-not-called
|
||||||
def __init__(self, layout: Any, is_backup: bool = False):
|
def __init__(self, layout: Any):
|
||||||
self.layout = layout
|
self.layout = layout
|
||||||
self.timer = loop.Timer()
|
self.timer = loop.Timer()
|
||||||
self.layout.attach_timer_fn(self.set_timer)
|
self.layout.attach_timer_fn(self.set_timer)
|
||||||
self.is_backup = is_backup
|
|
||||||
|
|
||||||
if __debug__ and self.is_backup:
|
|
||||||
self.notify_backup()
|
|
||||||
|
|
||||||
def set_timer(self, token: int, deadline: int) -> None:
|
def set_timer(self, token: int, deadline: int) -> None:
|
||||||
self.timer.schedule(deadline, token)
|
self.timer.schedule(deadline, token)
|
||||||
@ -60,44 +56,49 @@ class RustLayout(ui.Layout):
|
|||||||
if __debug__:
|
if __debug__:
|
||||||
|
|
||||||
def create_tasks(self) -> tuple[loop.AwaitableTask, ...]:
|
def create_tasks(self) -> tuple[loop.AwaitableTask, ...]:
|
||||||
from apps.debug import confirm_signal, input_signal
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
self.handle_timers(),
|
self.handle_timers(),
|
||||||
self.handle_input_and_rendering(),
|
self.handle_input_and_rendering(),
|
||||||
self.handle_swipe(),
|
self.handle_swipe(),
|
||||||
confirm_signal(),
|
self.handle_click_signal(),
|
||||||
input_signal(),
|
self.handle_result_signal(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def read_content(self) -> list[str]:
|
async def handle_result_signal(self) -> None:
|
||||||
result: list[str] = []
|
"""Enables sending arbitrary input - ui.Result.
|
||||||
|
|
||||||
|
Waits for `result_signal` and carries it out.
|
||||||
|
"""
|
||||||
|
from apps.debug import result_signal
|
||||||
|
from storage import debug as debug_storage
|
||||||
|
|
||||||
|
while True:
|
||||||
|
event_id, result = await result_signal()
|
||||||
|
debug_storage.new_layout_event_id = event_id
|
||||||
|
raise ui.Result(result)
|
||||||
|
|
||||||
|
def read_content_into(self, content_store: list[str]) -> None:
|
||||||
|
"""Reads all the strings/tokens received from Rust into given list."""
|
||||||
|
|
||||||
def callback(*args: Any) -> None:
|
def callback(*args: Any) -> None:
|
||||||
for arg in args:
|
for arg in args:
|
||||||
result.append(str(arg))
|
content_store.append(str(arg))
|
||||||
|
|
||||||
|
content_store.clear()
|
||||||
self.layout.trace(callback)
|
self.layout.trace(callback)
|
||||||
result = " ".join(result).split("\n")
|
|
||||||
return result
|
|
||||||
|
|
||||||
async def handle_swipe(self):
|
async def handle_swipe(self):
|
||||||
from apps.debug import notify_layout_change, swipe_signal
|
from apps.debug import notify_layout_change, swipe_signal
|
||||||
from trezor.ui import (
|
from trezor.enums import DebugSwipeDirection
|
||||||
SWIPE_UP,
|
|
||||||
SWIPE_DOWN,
|
|
||||||
SWIPE_LEFT,
|
|
||||||
SWIPE_RIGHT,
|
|
||||||
)
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
direction = await swipe_signal()
|
event_id, direction = await swipe_signal()
|
||||||
orig_x = orig_y = 120
|
orig_x = orig_y = 120
|
||||||
off_x, off_y = {
|
off_x, off_y = {
|
||||||
SWIPE_UP: (0, -30),
|
DebugSwipeDirection.UP: (0, -30),
|
||||||
SWIPE_DOWN: (0, 30),
|
DebugSwipeDirection.DOWN: (0, 30),
|
||||||
SWIPE_LEFT: (-30, 0),
|
DebugSwipeDirection.LEFT: (-30, 0),
|
||||||
SWIPE_RIGHT: (30, 0),
|
DebugSwipeDirection.RIGHT: (30, 0),
|
||||||
}[direction]
|
}[direction]
|
||||||
|
|
||||||
for event, x, y in (
|
for event, x, y in (
|
||||||
@ -110,26 +111,46 @@ class RustLayout(ui.Layout):
|
|||||||
if msg is not None:
|
if msg is not None:
|
||||||
raise ui.Result(msg)
|
raise ui.Result(msg)
|
||||||
|
|
||||||
if self.is_backup:
|
notify_layout_change(self, event_id)
|
||||||
self.notify_backup()
|
|
||||||
notify_layout_change(self)
|
|
||||||
|
|
||||||
def notify_backup(self):
|
async def _click(
|
||||||
from apps.debug import reset_current_words
|
self,
|
||||||
|
event_id: int | None,
|
||||||
|
x: int,
|
||||||
|
y: int,
|
||||||
|
hold_ms: int | None,
|
||||||
|
) -> Any:
|
||||||
|
from trezor import workflow
|
||||||
|
from apps.debug import notify_layout_change
|
||||||
|
from storage import debug as debug_storage
|
||||||
|
|
||||||
content = "\n".join(self.read_content())
|
self.layout.touch_event(io.TOUCH_START, x, y)
|
||||||
start = "< Paragraphs "
|
self._paint()
|
||||||
end = ">"
|
if hold_ms is not None:
|
||||||
start_pos = content.index(start)
|
await loop.sleep(hold_ms)
|
||||||
end_pos = content.index(end, start_pos)
|
msg = self.layout.touch_event(io.TOUCH_END, x, y)
|
||||||
words: list[str] = []
|
|
||||||
for line in content[start_pos + len(start) : end_pos].split("\n"):
|
if msg is not None:
|
||||||
line = line.strip()
|
debug_storage.new_layout_event_id = event_id
|
||||||
if not line:
|
raise ui.Result(msg)
|
||||||
continue
|
|
||||||
space_pos = line.index(" ")
|
# So that these presses will keep trezor awake
|
||||||
words.append(line[space_pos + 1 :])
|
# (it will not be locked after auto_lock_delay_ms)
|
||||||
reset_current_words.publish(words)
|
workflow.idle_timer.touch()
|
||||||
|
|
||||||
|
self._paint()
|
||||||
|
notify_layout_change(self, event_id)
|
||||||
|
|
||||||
|
async def handle_click_signal(self) -> None:
|
||||||
|
"""Enables clicking somewhere on the screen.
|
||||||
|
|
||||||
|
Waits for `click_signal` and carries it out.
|
||||||
|
"""
|
||||||
|
from apps.debug import click_signal
|
||||||
|
|
||||||
|
while True:
|
||||||
|
event_id, x, y, hold_ms = await click_signal()
|
||||||
|
await self._click(event_id, x, y, hold_ms)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
@ -144,12 +165,21 @@ class RustLayout(ui.Layout):
|
|||||||
|
|
||||||
if __debug__ and self.should_notify_layout_change:
|
if __debug__ and self.should_notify_layout_change:
|
||||||
from apps.debug import notify_layout_change
|
from apps.debug import notify_layout_change
|
||||||
|
from storage import debug as debug_storage
|
||||||
|
|
||||||
# notify about change and do not notify again until next await.
|
# notify about change and do not notify again until next await.
|
||||||
# (handle_rendering might be called multiple times in a single await,
|
# (handle_rendering might be called multiple times in a single await,
|
||||||
# because of the endless loop in __iter__)
|
# because of the endless loop in __iter__)
|
||||||
self.should_notify_layout_change = False
|
self.should_notify_layout_change = False
|
||||||
notify_layout_change(self)
|
|
||||||
|
# Possibly there is an event ID that caused the layout change,
|
||||||
|
# so notifying with this ID.
|
||||||
|
event_id = None
|
||||||
|
if debug_storage.new_layout_event_id is not None:
|
||||||
|
event_id = debug_storage.new_layout_event_id
|
||||||
|
debug_storage.new_layout_event_id = None
|
||||||
|
|
||||||
|
notify_layout_change(self, event_id)
|
||||||
|
|
||||||
# Turn the brightness on again.
|
# Turn the brightness on again.
|
||||||
ui.backlight_fade(self.BACKLIGHT_LEVEL)
|
ui.backlight_fade(self.BACKLIGHT_LEVEL)
|
||||||
|
@ -26,15 +26,11 @@ if __debug__:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def handle_debug_confirm(self) -> None:
|
async def handle_debug_confirm(self) -> None:
|
||||||
from apps.debug import confirm_signal
|
from apps.debug import result_signal
|
||||||
|
|
||||||
try:
|
_event_id, result = await result_signal()
|
||||||
await confirm_signal()
|
if result is not trezorui2.CONFIRMED:
|
||||||
except Result as r:
|
raise Result(result)
|
||||||
if r.value is not trezorui2.CONFIRMED:
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
for event, x, y in (
|
for event, x, y in (
|
||||||
(io.TOUCH_START, 220, 220),
|
(io.TOUCH_START, 220, 220),
|
||||||
|
@ -31,7 +31,11 @@ class HomescreenBase(RustLayout):
|
|||||||
|
|
||||||
# In __debug__ mode, ignore {confirm,swipe,input}_signal.
|
# In __debug__ mode, ignore {confirm,swipe,input}_signal.
|
||||||
def create_tasks(self) -> tuple[loop.AwaitableTask, ...]:
|
def create_tasks(self) -> tuple[loop.AwaitableTask, ...]:
|
||||||
return self.handle_timers(), self.handle_input_and_rendering()
|
return (
|
||||||
|
self.handle_timers(),
|
||||||
|
self.handle_input_and_rendering(),
|
||||||
|
self.handle_click_signal(), # so we can receive debug events
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Homescreen(HomescreenBase):
|
class Homescreen(HomescreenBase):
|
||||||
|
@ -73,7 +73,6 @@ async def show_share_words(
|
|||||||
title=title,
|
title=title,
|
||||||
pages=pages,
|
pages=pages,
|
||||||
),
|
),
|
||||||
is_backup=True,
|
|
||||||
),
|
),
|
||||||
"backup_words",
|
"backup_words",
|
||||||
ButtonRequestType.ResetDevice,
|
ButtonRequestType.ResetDevice,
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
# You should have received a copy of the License along with this library.
|
# You should have received a copy of the License along with this library.
|
||||||
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import textwrap
|
import textwrap
|
||||||
@ -57,133 +58,217 @@ if TYPE_CHECKING:
|
|||||||
protobuf.MessageType, Type[protobuf.MessageType], "MessageFilter"
|
protobuf.MessageType, Type[protobuf.MessageType], "MessageFilter"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
AnyDict = Dict[str, Any]
|
||||||
|
|
||||||
EXPECTED_RESPONSES_CONTEXT_LINES = 3
|
EXPECTED_RESPONSES_CONTEXT_LINES = 3
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class LayoutContent:
|
class UnstructuredJSONReader:
|
||||||
|
"""Contains data-parsing helpers for JSON data that have unknown structure."""
|
||||||
|
|
||||||
|
def __init__(self, json_str: str) -> None:
|
||||||
|
self.json_str = json_str
|
||||||
|
# We may not receive valid JSON, e.g. from an old model in upgrade tests
|
||||||
|
try:
|
||||||
|
self.dict: AnyDict = json.loads(json_str)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
self.dict = {}
|
||||||
|
|
||||||
|
def top_level_value(self, key: str) -> Any:
|
||||||
|
return self.dict[key]
|
||||||
|
|
||||||
|
def find_objects_with_key_and_value(self, key: str, value: Any) -> List["AnyDict"]:
|
||||||
|
def recursively_find(data: Any) -> Iterator[Any]:
|
||||||
|
if isinstance(data, dict):
|
||||||
|
if data.get(key) == value:
|
||||||
|
yield data
|
||||||
|
for val in data.values():
|
||||||
|
yield from recursively_find(val)
|
||||||
|
elif isinstance(data, list):
|
||||||
|
for item in data:
|
||||||
|
yield from recursively_find(item)
|
||||||
|
|
||||||
|
return list(recursively_find(self.dict))
|
||||||
|
|
||||||
|
def find_values_by_key(
|
||||||
|
self, key: str, only_type: Union[type, None] = None
|
||||||
|
) -> List[Any]:
|
||||||
|
def recursively_find(data: Any) -> Iterator[Any]:
|
||||||
|
if isinstance(data, dict):
|
||||||
|
if key in data:
|
||||||
|
yield data[key]
|
||||||
|
for val in data.values():
|
||||||
|
yield from recursively_find(val)
|
||||||
|
elif isinstance(data, list):
|
||||||
|
for item in data:
|
||||||
|
yield from recursively_find(item)
|
||||||
|
|
||||||
|
values = list(recursively_find(self.dict))
|
||||||
|
|
||||||
|
if only_type is not None:
|
||||||
|
values = [v for v in values if isinstance(v, only_type)]
|
||||||
|
|
||||||
|
return values
|
||||||
|
|
||||||
|
def find_unique_value_by_key(
|
||||||
|
self, key: str, default: Any, only_type: Union[type, None] = None
|
||||||
|
) -> Any:
|
||||||
|
values = self.find_values_by_key(key, only_type=only_type)
|
||||||
|
if not values:
|
||||||
|
return default
|
||||||
|
assert len(values) == 1
|
||||||
|
return values[0]
|
||||||
|
|
||||||
|
|
||||||
|
class LayoutContent(UnstructuredJSONReader):
|
||||||
"""Stores content of a layout as returned from Trezor.
|
"""Stores content of a layout as returned from Trezor.
|
||||||
|
|
||||||
Contains helper functions to extract specific parts of the layout.
|
Contains helper functions to extract specific parts of the layout.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, lines: Sequence[str]) -> None:
|
def __init__(self, json_tokens: Sequence[str]) -> None:
|
||||||
self.lines = list(lines)
|
json_str = "".join(json_tokens)
|
||||||
self.text = " ".join(self.lines)
|
super().__init__(json_str)
|
||||||
|
|
||||||
def get_title(self) -> str:
|
def main_component(self) -> str:
|
||||||
"""Get title of the layout.
|
"""Getting the main component of the layout."""
|
||||||
|
return self.top_level_value("component")
|
||||||
|
|
||||||
Title is located between "title" and "content" identifiers.
|
def all_components(self) -> List[str]:
|
||||||
Example: "< Frame title : RECOVERY SHARE #1 content : < SwipePage"
|
"""Getting all components of the layout."""
|
||||||
-> "RECOVERY SHARE #1"
|
return self.find_values_by_key("component", only_type=str)
|
||||||
|
|
||||||
|
def visible_screen(self) -> str:
|
||||||
|
"""String representation of a current screen content.
|
||||||
|
Example:
|
||||||
|
SIGN TRANSACTION
|
||||||
|
--------------------
|
||||||
|
You are about to
|
||||||
|
sign 3 actions.
|
||||||
|
********************
|
||||||
|
Icon:cancel [Cancel], --- [None], CONFIRM [Confirm]
|
||||||
"""
|
"""
|
||||||
match = re.search(r"title : (.*?) content :", self.text)
|
title_separator = f"\n{20*'-'}\n"
|
||||||
if not match:
|
btn_separator = f"\n{20*'*'}\n"
|
||||||
return ""
|
|
||||||
return match.group(1).strip()
|
|
||||||
|
|
||||||
def get_content(self, tag_name: str = "Paragraphs", raw: bool = False) -> str:
|
visible = ""
|
||||||
"""Get text of the main screen content of the layout."""
|
if self.title():
|
||||||
content = "".join(self._get_content_lines(tag_name, raw))
|
visible += self.title()
|
||||||
if not raw and content.endswith(" "):
|
visible += title_separator
|
||||||
# Stripping possible space at the end
|
visible += self.screen_content()
|
||||||
content = content[:-1]
|
visible_buttons = self.button_contents()
|
||||||
return content
|
if visible_buttons:
|
||||||
|
visible += btn_separator
|
||||||
|
visible += ", ".join(visible_buttons)
|
||||||
|
|
||||||
def get_button_texts(self) -> List[str]:
|
return visible
|
||||||
"""Get text of all buttons in the layout.
|
|
||||||
|
|
||||||
Example button: "< Button text : LADYBUG >"
|
def title(self) -> str:
|
||||||
-> ["LADYBUG"]
|
"""Getting text that is displayed as a title."""
|
||||||
|
# There could be possibly subtitle as well
|
||||||
|
title_parts: List[str] = []
|
||||||
|
|
||||||
|
def _get_str_or_dict_text(key: str) -> str:
|
||||||
|
value = self.find_unique_value_by_key(key, "")
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return value["text"]
|
||||||
|
return value
|
||||||
|
|
||||||
|
title = _get_str_or_dict_text("title")
|
||||||
|
if title:
|
||||||
|
title_parts.append(title)
|
||||||
|
|
||||||
|
subtitle = _get_str_or_dict_text("subtitle")
|
||||||
|
if subtitle:
|
||||||
|
title_parts.append(subtitle)
|
||||||
|
|
||||||
|
return "\n".join(title_parts)
|
||||||
|
|
||||||
|
def text_content(self) -> str:
|
||||||
|
"""What is on the screen, in one long string, so content can be
|
||||||
|
asserted regardless of newlines.
|
||||||
"""
|
"""
|
||||||
return re.findall(r"< Button text : +(.*?) >", self.text)
|
return self.screen_content().replace("\n", " ")
|
||||||
|
|
||||||
def get_seed_words(self) -> List[str]:
|
def screen_content(self) -> str:
|
||||||
|
"""Getting text that is displayed in the main part of the screen.
|
||||||
|
Preserving the line breaks.
|
||||||
|
"""
|
||||||
|
main_text_blocks: List[str] = []
|
||||||
|
paragraphs = self.raw_content_paragraphs()
|
||||||
|
if not paragraphs:
|
||||||
|
return self.main_component()
|
||||||
|
for par in paragraphs:
|
||||||
|
par_content = ""
|
||||||
|
for line_or_newline in par:
|
||||||
|
par_content += line_or_newline
|
||||||
|
par_content.replace("\n", " ")
|
||||||
|
main_text_blocks.append(par_content)
|
||||||
|
return "\n".join(main_text_blocks)
|
||||||
|
|
||||||
|
def raw_content_paragraphs(self) -> Union[List[List[str]], None]:
|
||||||
|
"""Getting raw paragraphs as sent from Rust."""
|
||||||
|
return self.find_unique_value_by_key("paragraphs", default=None, only_type=list)
|
||||||
|
|
||||||
|
def button_contents(self) -> List[str]:
|
||||||
|
"""Getting list of button contents."""
|
||||||
|
buttons: List[str] = []
|
||||||
|
button_objects = self.find_objects_with_key_and_value("component", "Button")
|
||||||
|
for button in button_objects:
|
||||||
|
if button.get("icon"):
|
||||||
|
buttons.append("ICON")
|
||||||
|
elif "text" in button:
|
||||||
|
buttons.append(button["text"])
|
||||||
|
return buttons
|
||||||
|
|
||||||
|
def seed_words(self) -> List[str]:
|
||||||
"""Get all the seed words on the screen in order.
|
"""Get all the seed words on the screen in order.
|
||||||
|
|
||||||
Example content: "1. ladybug 2. acid 3. academic 4. afraid"
|
Example content: "1. ladybug\n2. acid\n3. academic\n4. afraid"
|
||||||
-> ["ladybug", "acid", "academic", "afraid"]
|
-> ["ladybug", "acid", "academic", "afraid"]
|
||||||
"""
|
"""
|
||||||
return re.findall(r"\d+\. (\w+)\b", self.get_content())
|
words: List[str] = []
|
||||||
|
for line in self.screen_content().split("\n"):
|
||||||
|
# Dot after index is optional (present on TT, not on TR)
|
||||||
|
match = re.match(r"^\d+\.? (\w+)$", line)
|
||||||
|
if match:
|
||||||
|
words.append(match.group(1))
|
||||||
|
return words
|
||||||
|
|
||||||
def get_page_count(self) -> int:
|
def pin(self) -> str:
|
||||||
|
"""Get PIN from the layout."""
|
||||||
|
assert self.main_component() == "PinKeyboard"
|
||||||
|
return self.find_unique_value_by_key("pin", default="", only_type=str)
|
||||||
|
|
||||||
|
def passphrase(self) -> str:
|
||||||
|
"""Get passphrase from the layout."""
|
||||||
|
assert self.main_component() == "PassphraseKeyboard"
|
||||||
|
return self.find_unique_value_by_key("passphrase", default="", only_type=str)
|
||||||
|
|
||||||
|
def page_count(self) -> int:
|
||||||
"""Get number of pages for the layout."""
|
"""Get number of pages for the layout."""
|
||||||
return self._get_number("page_count")
|
return (
|
||||||
|
self.find_unique_value_by_key(
|
||||||
|
"scrollbar_page_count", default=0, only_type=int
|
||||||
|
)
|
||||||
|
or self.find_unique_value_by_key("page_count", default=0, only_type=int)
|
||||||
|
or 1
|
||||||
|
)
|
||||||
|
|
||||||
def get_active_page(self) -> int:
|
def active_page(self) -> int:
|
||||||
"""Get current page index of the layout."""
|
"""Get current page index of the layout."""
|
||||||
return self._get_number("active_page")
|
return self.find_unique_value_by_key("active_page", default=0, only_type=int)
|
||||||
|
|
||||||
def _get_number(self, key: str) -> int:
|
def tt_pin_digits_order(self) -> str:
|
||||||
"""Get number connected with a specific key."""
|
"""In what order the PIN buttons are shown on the screen. Only for TT."""
|
||||||
match = re.search(rf"{key} : +(\d+)", self.text)
|
return self.top_level_value("digits_order")
|
||||||
if not match:
|
|
||||||
return 0
|
|
||||||
return int(match.group(1))
|
|
||||||
|
|
||||||
def _get_content_lines(
|
|
||||||
self, tag_name: str = "Paragraphs", raw: bool = False
|
|
||||||
) -> List[str]:
|
|
||||||
"""Get lines of the main screen content of the layout."""
|
|
||||||
|
|
||||||
# First line should have content after the tag, last line does not store content
|
|
||||||
tag = f"< {tag_name}"
|
|
||||||
for i in range(len(self.lines)):
|
|
||||||
if tag in self.lines[i]:
|
|
||||||
first_line = self.lines[i].split(tag)[1]
|
|
||||||
all_lines = [first_line] + self.lines[i + 1 : -1]
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
all_lines = self.lines[1:-1]
|
|
||||||
|
|
||||||
if raw:
|
|
||||||
return all_lines
|
|
||||||
else:
|
|
||||||
return [_clean_line(line) for line in all_lines]
|
|
||||||
|
|
||||||
|
|
||||||
def _clean_line(line: str) -> str:
|
|
||||||
"""Cleaning the layout line for extra spaces, hyphens and ellipsis.
|
|
||||||
|
|
||||||
Line usually comes in the form of " <content> ", with trailing spaces
|
|
||||||
at both ends. It may end with a hyphen (" - ") or ellipsis (" ... ").
|
|
||||||
|
|
||||||
Hyphen means the word was split to the next line, ellipsis signals
|
|
||||||
the text continuing on the next page.
|
|
||||||
"""
|
|
||||||
# Deleting space at the beginning
|
|
||||||
if line.startswith(" "):
|
|
||||||
line = line[1:]
|
|
||||||
|
|
||||||
# Deleting a hyphen at the end, together with the space
|
|
||||||
# before it, so it will be tightly connected with the next line
|
|
||||||
if line.endswith(" - "):
|
|
||||||
line = line[:-3]
|
|
||||||
|
|
||||||
# Deleting the ellipsis at the end (but preserving the space there)
|
|
||||||
if line.endswith(" ... "):
|
|
||||||
line = line[:-4]
|
|
||||||
|
|
||||||
return line
|
|
||||||
|
|
||||||
|
|
||||||
def multipage_content(layouts: List[LayoutContent]) -> str:
|
def multipage_content(layouts: List[LayoutContent]) -> str:
|
||||||
"""Get overall content from multiple-page layout."""
|
"""Get overall content from multiple-page layout."""
|
||||||
final_text = ""
|
return "".join(layout.text_content() for layout in layouts)
|
||||||
for layout in layouts:
|
|
||||||
final_text += layout.get_content()
|
|
||||||
# When the raw content of the page ends with ellipsis,
|
|
||||||
# we need to add a space to separate it with the next page
|
|
||||||
if layout.get_content(raw=True).endswith("... "):
|
|
||||||
final_text += " "
|
|
||||||
|
|
||||||
# Stripping possible space at the end of last page
|
|
||||||
if final_text.endswith(" "):
|
|
||||||
final_text = final_text[:-1]
|
|
||||||
|
|
||||||
return final_text
|
|
||||||
|
|
||||||
|
|
||||||
class DebugLink:
|
class DebugLink:
|
||||||
@ -194,6 +279,7 @@ class DebugLink:
|
|||||||
|
|
||||||
# To be set by TrezorClientDebugLink (is not known during creation time)
|
# To be set by TrezorClientDebugLink (is not known during creation time)
|
||||||
self.model: Optional[str] = None
|
self.model: Optional[str] = None
|
||||||
|
self.version: Tuple[int, int, int] = (0, 0, 0)
|
||||||
|
|
||||||
# Where screenshots are being saved
|
# Where screenshots are being saved
|
||||||
self.screenshot_recording_dir: Optional[str] = None
|
self.screenshot_recording_dir: Optional[str] = None
|
||||||
@ -207,6 +293,16 @@ class DebugLink:
|
|||||||
self.screen_text_file: Optional[Path] = None
|
self.screen_text_file: Optional[Path] = None
|
||||||
self.last_screen_content = ""
|
self.last_screen_content = ""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def legacy_ui(self) -> bool:
|
||||||
|
"""Differences between UI1 and UI2."""
|
||||||
|
return self.version < (2, 6, 0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def legacy_debug(self) -> bool:
|
||||||
|
"""Differences in handling debug events and LayoutContent."""
|
||||||
|
return self.version < (2, 6, 1)
|
||||||
|
|
||||||
def set_screen_text_file(self, file_path: Optional[Path]) -> None:
|
def set_screen_text_file(self, file_path: Optional[Path]) -> None:
|
||||||
if file_path is not None:
|
if file_path is not None:
|
||||||
file_path.write_bytes(b"")
|
file_path.write_bytes(b"")
|
||||||
@ -248,19 +344,33 @@ class DebugLink:
|
|||||||
return self._call(messages.DebugLinkGetState())
|
return self._call(messages.DebugLinkGetState())
|
||||||
|
|
||||||
def read_layout(self) -> LayoutContent:
|
def read_layout(self) -> LayoutContent:
|
||||||
return LayoutContent(self.state().layout_lines)
|
return LayoutContent(self.state().tokens or [])
|
||||||
|
|
||||||
|
def wait_layout(self, wait_for_external_change: bool = False) -> LayoutContent:
|
||||||
|
# Next layout change will be caused by external event
|
||||||
|
# (e.g. device being auto-locked or as a result of device_handler.run(xxx))
|
||||||
|
# and not by our debug actions/decisions.
|
||||||
|
# Resetting the debug state so we wait for the next layout change
|
||||||
|
# (and do not return the current state).
|
||||||
|
if wait_for_external_change:
|
||||||
|
self.reset_debug_events()
|
||||||
|
|
||||||
def wait_layout(self) -> LayoutContent:
|
|
||||||
obj = self._call(messages.DebugLinkGetState(wait_layout=True))
|
obj = self._call(messages.DebugLinkGetState(wait_layout=True))
|
||||||
if isinstance(obj, messages.Failure):
|
if isinstance(obj, messages.Failure):
|
||||||
raise TrezorFailure(obj)
|
raise TrezorFailure(obj)
|
||||||
return LayoutContent(obj.layout_lines)
|
return LayoutContent(obj.tokens)
|
||||||
|
|
||||||
|
def reset_debug_events(self) -> None:
|
||||||
|
# Only supported on TT and above certain version
|
||||||
|
if self.model == "T" and not self.legacy_debug:
|
||||||
|
return self._call(messages.DebugLinkResetDebugEvents())
|
||||||
|
return None
|
||||||
|
|
||||||
def synchronize_at(self, layout_text: str, timeout: float = 5) -> LayoutContent:
|
def synchronize_at(self, layout_text: str, timeout: float = 5) -> LayoutContent:
|
||||||
now = time.monotonic()
|
now = time.monotonic()
|
||||||
while True:
|
while True:
|
||||||
layout = self.read_layout()
|
layout = self.read_layout()
|
||||||
if layout_text in layout.text:
|
if layout_text in layout.json_str:
|
||||||
return layout
|
return layout
|
||||||
if time.monotonic() - now > timeout:
|
if time.monotonic() - now > timeout:
|
||||||
raise RuntimeError("Timeout waiting for layout")
|
raise RuntimeError("Timeout waiting for layout")
|
||||||
@ -293,10 +403,6 @@ class DebugLink:
|
|||||||
state = self._call(messages.DebugLinkGetState(wait_word_list=True))
|
state = self._call(messages.DebugLinkGetState(wait_word_list=True))
|
||||||
return state.reset_word
|
return state.reset_word
|
||||||
|
|
||||||
def read_reset_word_pos(self) -> int:
|
|
||||||
state = self._call(messages.DebugLinkGetState(wait_word_pos=True))
|
|
||||||
return state.reset_word_pos
|
|
||||||
|
|
||||||
def input(
|
def input(
|
||||||
self,
|
self,
|
||||||
word: Optional[str] = None,
|
word: Optional[str] = None,
|
||||||
@ -312,7 +418,9 @@ class DebugLink:
|
|||||||
|
|
||||||
args = sum(a is not None for a in (word, button, swipe, x))
|
args = sum(a is not None for a in (word, button, swipe, x))
|
||||||
if args != 1:
|
if args != 1:
|
||||||
raise ValueError("Invalid input - must use one of word, button, swipe")
|
raise ValueError(
|
||||||
|
"Invalid input - must use one of word, button, swipe, click(x,y)"
|
||||||
|
)
|
||||||
|
|
||||||
decision = messages.DebugLinkDecision(
|
decision = messages.DebugLinkDecision(
|
||||||
button=button, swipe=swipe, input=word, x=x, y=y, wait=wait, hold_ms=hold_ms
|
button=button, swipe=swipe, input=word, x=x, y=y, wait=wait, hold_ms=hold_ms
|
||||||
@ -320,7 +428,7 @@ class DebugLink:
|
|||||||
|
|
||||||
ret = self._call(decision, nowait=not wait)
|
ret = self._call(decision, nowait=not wait)
|
||||||
if ret is not None:
|
if ret is not None:
|
||||||
return LayoutContent(ret.lines)
|
return LayoutContent(ret.tokens)
|
||||||
|
|
||||||
# Getting the current screen after the (nowait) decision
|
# Getting the current screen after the (nowait) decision
|
||||||
self.save_current_screen_if_relevant(wait=False)
|
self.save_current_screen_if_relevant(wait=False)
|
||||||
@ -336,22 +444,23 @@ class DebugLink:
|
|||||||
layout = self.wait_layout()
|
layout = self.wait_layout()
|
||||||
else:
|
else:
|
||||||
layout = self.read_layout()
|
layout = self.read_layout()
|
||||||
self.save_debug_screen(layout.lines)
|
self.save_debug_screen(layout.visible_screen())
|
||||||
|
|
||||||
def save_debug_screen(self, lines: List[str]) -> None:
|
def save_debug_screen(self, screen_content: str) -> None:
|
||||||
if self.screen_text_file is None:
|
if self.screen_text_file is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
content = "\n".join(lines)
|
if not self.screen_text_file.exists():
|
||||||
|
self.screen_text_file.write_bytes(b"")
|
||||||
|
|
||||||
# Not writing the same screen twice
|
# Not writing the same screen twice
|
||||||
if content == self.last_screen_content:
|
if screen_content == self.last_screen_content:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.last_screen_content = content
|
self.last_screen_content = screen_content
|
||||||
|
|
||||||
with open(self.screen_text_file, "a") as f:
|
with open(self.screen_text_file, "a") as f:
|
||||||
f.write(content)
|
f.write(screen_content)
|
||||||
f.write("\n" + 80 * "/" + "\n")
|
f.write("\n" + 80 * "/" + "\n")
|
||||||
|
|
||||||
# Type overloads make sure that when we supply `wait=True` into `click()`,
|
# Type overloads make sure that when we supply `wait=True` into `click()`,
|
||||||
@ -371,6 +480,14 @@ class DebugLink:
|
|||||||
x, y = click
|
x, y = click
|
||||||
return self.input(x=x, y=y, wait=wait)
|
return self.input(x=x, y=y, wait=wait)
|
||||||
|
|
||||||
|
# Made into separate function as `hold_ms: Optional[int]` in `click`
|
||||||
|
# was causing problems with @overload
|
||||||
|
def click_hold(
|
||||||
|
self, click: Tuple[int, int], hold_ms: int
|
||||||
|
) -> Optional[LayoutContent]:
|
||||||
|
x, y = click
|
||||||
|
return self.input(x=x, y=y, hold_ms=hold_ms, wait=True)
|
||||||
|
|
||||||
def press_yes(self, wait: bool = False) -> None:
|
def press_yes(self, wait: bool = False) -> None:
|
||||||
self.input(button=messages.DebugButton.YES, wait=wait)
|
self.input(button=messages.DebugButton.YES, wait=wait)
|
||||||
|
|
||||||
@ -538,6 +655,7 @@ class DebugUI:
|
|||||||
if br.code == messages.ButtonRequestType.PinEntry:
|
if br.code == messages.ButtonRequestType.PinEntry:
|
||||||
self.debuglink.input(self.get_pin())
|
self.debuglink.input(self.get_pin())
|
||||||
else:
|
else:
|
||||||
|
# Paginating (going as further as possible) and pressing Yes
|
||||||
if br.pages is not None:
|
if br.pages is not None:
|
||||||
for _ in range(br.pages - 1):
|
for _ in range(br.pages - 1):
|
||||||
self.debuglink.swipe_up(wait=True)
|
self.debuglink.swipe_up(wait=True)
|
||||||
@ -688,7 +806,9 @@ class TrezorClientDebugLink(TrezorClient):
|
|||||||
super().__init__(transport, ui=self.ui)
|
super().__init__(transport, ui=self.ui)
|
||||||
|
|
||||||
# So that we can choose right screenshotting logic (T1 vs TT)
|
# So that we can choose right screenshotting logic (T1 vs TT)
|
||||||
|
# and know the supported debug capabilities
|
||||||
self.debug.model = self.features.model
|
self.debug.model = self.features.model
|
||||||
|
self.debug.version = self.version
|
||||||
|
|
||||||
def reset_debug_features(self) -> None:
|
def reset_debug_features(self) -> None:
|
||||||
"""Prepare the debugging client for a new testcase.
|
"""Prepare the debugging client for a new testcase.
|
||||||
|
@ -16,22 +16,22 @@ def enter_word(
|
|||||||
return debug.click(buttons.CONFIRM_WORD, wait=True)
|
return debug.click(buttons.CONFIRM_WORD, wait=True)
|
||||||
|
|
||||||
|
|
||||||
def confirm_recovery(debug: "DebugLink", legacy_ui: bool = False) -> None:
|
def confirm_recovery(debug: "DebugLink") -> None:
|
||||||
if not legacy_ui:
|
if not debug.legacy_ui and not debug.legacy_debug:
|
||||||
layout = debug.wait_layout()
|
layout = debug.wait_layout()
|
||||||
assert layout.title().startswith("WALLET RECOVERY")
|
assert layout.title().startswith("WALLET RECOVERY")
|
||||||
debug.click(buttons.OK, wait=True)
|
debug.click(buttons.OK, wait=True)
|
||||||
|
|
||||||
|
|
||||||
def select_number_of_words(
|
def select_number_of_words(debug: "DebugLink", num_of_words: int = 20) -> None:
|
||||||
debug: "DebugLink", num_of_words: int = 20, legacy_ui: bool = False
|
|
||||||
) -> None:
|
|
||||||
# select number of words
|
# select number of words
|
||||||
if not legacy_ui:
|
if not debug.legacy_ui and not debug.legacy_debug:
|
||||||
assert "select the number of words" in debug.read_layout().text_content()
|
assert "select the number of words" in debug.read_layout().text_content()
|
||||||
layout = debug.click(buttons.OK, wait=True)
|
layout = debug.click(buttons.OK, wait=True)
|
||||||
if legacy_ui:
|
if debug.legacy_ui:
|
||||||
assert layout.json_str == "WordSelector"
|
assert layout.json_str == "WordSelector"
|
||||||
|
elif debug.version < (2, 6, 1):
|
||||||
|
assert "SelectWordCount" in layout.json_str
|
||||||
else:
|
else:
|
||||||
# Two title options
|
# Two title options
|
||||||
assert layout.title() in ("SEED CHECK", "WALLET RECOVERY")
|
assert layout.title() in ("SEED CHECK", "WALLET RECOVERY")
|
||||||
@ -45,20 +45,20 @@ def select_number_of_words(
|
|||||||
coords = buttons.grid34(index % 3, index // 3)
|
coords = buttons.grid34(index % 3, index // 3)
|
||||||
layout = debug.click(coords, wait=True)
|
layout = debug.click(coords, wait=True)
|
||||||
|
|
||||||
if not legacy_ui:
|
if not debug.legacy_ui and not debug.legacy_debug:
|
||||||
if num_of_words in (20, 33):
|
if num_of_words in (20, 33):
|
||||||
assert "Enter any share" in layout.text_content()
|
assert "Enter any share" in layout.text_content()
|
||||||
else:
|
else:
|
||||||
assert "enter your recovery seed" in layout.text_content()
|
assert "enter your recovery seed" in layout.text_content()
|
||||||
|
|
||||||
|
|
||||||
def enter_share(
|
def enter_share(debug: "DebugLink", share: str) -> "LayoutContent":
|
||||||
debug: "DebugLink", share: str, legacy_ui: bool = False
|
|
||||||
) -> "LayoutContent":
|
|
||||||
layout = debug.click(buttons.OK, wait=True)
|
layout = debug.click(buttons.OK, wait=True)
|
||||||
|
|
||||||
if legacy_ui:
|
if debug.legacy_ui:
|
||||||
assert layout.json_str == "Slip39Keyboard"
|
assert layout.json_str == "Slip39Keyboard"
|
||||||
|
elif debug.legacy_debug:
|
||||||
|
assert "MnemonicKeyboard" in layout.json_str
|
||||||
else:
|
else:
|
||||||
assert layout.main_component() == "MnemonicKeyboard"
|
assert layout.main_component() == "MnemonicKeyboard"
|
||||||
|
|
||||||
|
@ -209,6 +209,9 @@ def client(
|
|||||||
request.session.shouldstop = "Failed to communicate with Trezor"
|
request.session.shouldstop = "Failed to communicate with Trezor"
|
||||||
pytest.fail("Failed to communicate with Trezor")
|
pytest.fail("Failed to communicate with Trezor")
|
||||||
|
|
||||||
|
# Resetting all the debug events to not be influenced by previous test
|
||||||
|
_raw_client.debug.reset_debug_events()
|
||||||
|
|
||||||
if test_ui:
|
if test_ui:
|
||||||
# we need to reseed before the wipe
|
# we need to reseed before the wipe
|
||||||
_raw_client.debug.reseed(0)
|
_raw_client.debug.reseed(0)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Any, Callable
|
||||||
|
|
||||||
from trezorlib.client import PASSPHRASE_ON_DEVICE
|
from trezorlib.client import PASSPHRASE_ON_DEVICE
|
||||||
from trezorlib.transport import udp
|
from trezorlib.transport import udp
|
||||||
@ -42,10 +42,15 @@ class BackgroundDeviceHandler:
|
|||||||
self.client.ui = NullUI # type: ignore [NullUI is OK UI]
|
self.client.ui = NullUI # type: ignore [NullUI is OK UI]
|
||||||
self.client.watch_layout(True)
|
self.client.watch_layout(True)
|
||||||
|
|
||||||
def run(self, function, *args, **kwargs) -> None:
|
def run(self, function: Callable[..., Any], *args: Any, **kwargs: Any) -> None:
|
||||||
|
"""Runs some function that interacts with a device.
|
||||||
|
|
||||||
|
Makes sure the UI is updated before returning.
|
||||||
|
"""
|
||||||
if self.task is not None:
|
if self.task is not None:
|
||||||
raise RuntimeError("Wait for previous task first")
|
raise RuntimeError("Wait for previous task first")
|
||||||
self.task = self._pool.submit(function, self.client, *args, **kwargs)
|
self.task = self._pool.submit(function, self.client, *args, **kwargs)
|
||||||
|
self.debuglink().wait_layout(wait_for_external_change=True)
|
||||||
|
|
||||||
def kill_task(self) -> None:
|
def kill_task(self) -> None:
|
||||||
if self.task is not None:
|
if self.task is not None:
|
||||||
|
@ -309,14 +309,10 @@ def test_upgrade_shamir_recovery(gen: str, tag: Optional[str]):
|
|||||||
|
|
||||||
device_handler.run(device.recover, pin_protection=False)
|
device_handler.run(device.recover, pin_protection=False)
|
||||||
|
|
||||||
# Flow is different for old UI and new UI
|
recovery.confirm_recovery(debug)
|
||||||
legacy_ui = emu.client.version < (2, 5, 4)
|
recovery.select_number_of_words(debug)
|
||||||
|
layout = recovery.enter_share(debug, MNEMONIC_SLIP39_BASIC_20_3of6[0])
|
||||||
recovery.confirm_recovery(debug, legacy_ui=legacy_ui)
|
if not debug.legacy_ui and not debug.legacy_debug:
|
||||||
recovery.select_number_of_words(debug, legacy_ui=legacy_ui)
|
|
||||||
layout = recovery.enter_share(
|
|
||||||
debug, MNEMONIC_SLIP39_BASIC_20_3of6[0], legacy_ui=legacy_ui
|
|
||||||
)
|
|
||||||
assert "2 more shares" in layout.text_content()
|
assert "2 more shares" in layout.text_content()
|
||||||
|
|
||||||
device_id = emu.client.features.device_id
|
device_id = emu.client.features.device_id
|
||||||
|
Loading…
Reference in New Issue
Block a user