diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index 801cefb551..c44918dc8c 100644 --- a/.github/workflows/core.yml +++ b/.github/workflows/core.yml @@ -300,7 +300,7 @@ jobs: - run: chmod +x core/build/unix/trezor-emu-core* - uses: ./.github/actions/environment - name: Start Tropic model - if: ${{ env.TREZOR_MODEL == 'T3W1' }} + if: ${{ env.TREZOR_MODEL == 'T3W1' && env.ACTIONS_DO_UI_TEST != 'true' }} # ACTIONS_DO_UI_TEST refers to the test_emu_ui_multicore below which uses --control-emulators and starts tvl internally run: | nix-shell --arg fullDeps true --run "cd vendor/ts-tvl && poetry env use 3.12 && poetry install && poetry run model_server tcp -c ../../tests/tropic_model/config.yml > ../../tests/trezor-tropic-model.log 2>&1 &" - run: nix-shell --run "uv run make -C core ${{ env.ACTIONS_DO_UI_TEST == 'true' && 'test_emu_ui_multicore' || 'test_emu' }}" diff --git a/core/embed/projects/prodtest/main.c b/core/embed/projects/prodtest/main.c index 9455b54914..d0cade1b54 100644 --- a/core/embed/projects/prodtest/main.c +++ b/core/embed/projects/prodtest/main.c @@ -191,7 +191,11 @@ static void drivers_init(void) { ble_init(); #endif #ifdef USE_TROPIC +#ifdef TREZOR_EMULATOR + tropic_init(28992); +#else tropic_init(); +#endif tropic_wait_for_ready(); #endif #ifdef USE_HW_REVISION diff --git a/core/embed/projects/unix/main.c b/core/embed/projects/unix/main.c index 3e494a6509..a7d515ab04 100644 --- a/core/embed/projects/unix/main.c +++ b/core/embed/projects/unix/main.c @@ -504,7 +504,7 @@ static int sdl_event_filter(void *userdata, SDL_Event *event) { return 1; } -void drivers_init() { +void drivers_init(uint16_t tropic_model_port) { flash_init(); flash_otp_init(); @@ -521,7 +521,7 @@ void drivers_init() { #endif #ifdef USE_TROPIC - tropic_init(); + tropic_init(tropic_model_port); #endif usb_configure(NULL); @@ -531,7 +531,7 @@ void drivers_init() { // The function is called from the Rust before the test main function is run. void rust_tests_c_setup(void) { system_init(NULL); - drivers_init(); + drivers_init(28992); } MP_NOINLINE int main_(int argc, char **argv) { @@ -559,7 +559,23 @@ MP_NOINLINE int main_(int argc, char **argv) { system_init(&rsod_panic_handler); - drivers_init(); + char *tropic_model_port_str = getenv("TROPIC_MODEL_PORT"); + uint16_t tropic_model_port; + if (tropic_model_port_str == NULL) { + tropic_model_port = 28992; + } else { + char *endptr; + long port_long = strtol(tropic_model_port_str, &endptr, 10); + + if (*endptr != '\0' || port_long < 0 || port_long > 65535) { + printf("FATAL: invalid TROPIC_MODEL_PORT\n"); + exit(1); + } + + tropic_model_port = (uint16_t)port_long; + } + + drivers_init(tropic_model_port); SDL_SetEventFilter(sdl_event_filter, NULL); diff --git a/core/embed/sec/tropic/inc/sec/tropic.h b/core/embed/sec/tropic/inc/sec/tropic.h index 5d1f0d0200..fc0edae8be 100644 --- a/core/embed/sec/tropic/inc/sec/tropic.h +++ b/core/embed/sec/tropic/inc/sec/tropic.h @@ -61,7 +61,11 @@ #ifdef KERNEL_MODE +#ifdef TREZOR_EMULATOR +bool tropic_init(uint16_t port); +#else bool tropic_init(void); +#endif void tropic_deinit(void); diff --git a/core/embed/sec/tropic/tropic.c b/core/embed/sec/tropic/tropic.c index 6fb0585e28..04ba6ec784 100644 --- a/core/embed/sec/tropic/tropic.c +++ b/core/embed/sec/tropic/tropic.c @@ -185,7 +185,11 @@ bool tropic_session_start(void) { return false; } +#ifdef TREZOR_EMULATOR +bool tropic_init(uint16_t port) { +#else bool tropic_init(void) { +#endif tropic_driver_t *drv = &g_tropic_driver; if (drv->initialized) { @@ -194,7 +198,7 @@ bool tropic_init(void) { #ifdef TREZOR_EMULATOR drv->device.addr = inet_addr("127.0.0.1"); - drv->device.port = 28992; + drv->device.port = port; drv->handle.l2.device = &drv->device; #endif diff --git a/python/src/trezorlib/_internal/emulator.py b/python/src/trezorlib/_internal/emulator.py index 07c3193f1c..08499228a1 100644 --- a/python/src/trezorlib/_internal/emulator.py +++ b/python/src/trezorlib/_internal/emulator.py @@ -17,6 +17,7 @@ import atexit import logging import os +import socket import subprocess import time from pathlib import Path @@ -28,6 +29,7 @@ from ..transport.udp import UdpTransport LOG = logging.getLogger(__name__) +TROPIC_MODEL_WAIT_TIME = 10 EMULATOR_WAIT_TIME = 60 _RUNNING_PIDS = set() @@ -47,6 +49,97 @@ def _rm_f(path: Path) -> None: pass +class TropicModel: + def __init__( + self, + workdir: Path, + port: int, + configfile: str, + logfile: Union[TextIO, str, Path], + ) -> None: + self.workdir = workdir + self.port = port + self.configfile = configfile + self.logfile = logfile + self.process: Optional[subprocess.Popen] = None + + def start(self) -> None: + self.process = self._launch_process() + _RUNNING_PIDS.add(self.process) + try: + self._wait_until_ready() + except TimeoutError: + # Assuming that after the default, the process is stuck + LOG.warning( + f"Tropic model did not come up after {TROPIC_MODEL_WAIT_TIME} seconds" + ) + self.process.kill() + raise + + def stop(self) -> None: + if self.process: + LOG.info("Terminating Tropic model...") + start = time.monotonic() + self.process.terminate() + try: + self.process.wait(TROPIC_MODEL_WAIT_TIME) + end = time.monotonic() + LOG.info(f"Tropic model shut down after {end - start:.3f} seconds") + except subprocess.TimeoutExpired: + LOG.info("Tropic model seems stuck. Sending kill signal.") + self.process.kill() + _RUNNING_PIDS.remove(self.process) + + def _launch_process(self) -> subprocess.Popen: + # Opening the file if it is not already opened + if hasattr(self.logfile, "write"): + output = self.logfile + else: + assert isinstance(self.logfile, (str, Path)) + output = open(self.logfile, "w") + + return subprocess.Popen( + [ + "model_server", + "tcp", + "-c", + self.configfile, + "-p", + str(self.port), + ], + cwd=self.workdir, + stdout=cast(TextIO, output), + stderr=subprocess.STDOUT, + ) + + def _wait_until_ready(self, timeout: float = TROPIC_MODEL_WAIT_TIME) -> None: + assert self.process is not None, "Tropic model not started" + LOG.info("Waiting for Tropic model to come up...") + start = time.monotonic() + while True: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.settimeout(1.0) + # simply check whether we can connect to the TCP port + # where we told the model to listen + result = s.connect_ex(("127.0.0.1", self.port)) + if result == 0: + # seems that even if the model is listening for connections + # it sometimes needs up to 2 seconds more + # before it actually correctly processes requests + time.sleep(2) + break + if self.process.poll() is not None: + raise RuntimeError("Tropic model process died") + + elapsed = time.monotonic() - start + if elapsed >= timeout: + raise TimeoutError("Can't connect to Tropic model") + + time.sleep(0.1) + + LOG.info(f"Emulator ready after {time.monotonic() - start:.3f} seconds") + + class Emulator: STORAGE_FILENAME: str @@ -96,6 +189,12 @@ class Emulator: # To save all screenshots properly in one directory between restarts self.restart_amount = 0 + def start_tropic_model(self) -> None: + pass + + def stop_tropic_model(self) -> None: + pass + @property def client(self) -> TrezorClientDebugLink: """So that type-checkers do not see `client` as `Optional`. @@ -117,7 +216,7 @@ class Emulator: def _get_transport(self) -> UdpTransport: return UdpTransport(f"127.0.0.1:{self.port}") - def wait_until_ready(self, timeout: float = EMULATOR_WAIT_TIME) -> None: + def _wait_until_ready(self, timeout: float = EMULATOR_WAIT_TIME) -> None: assert self.process is not None, "Emulator not started" self.transport.open() LOG.info("Waiting for emulator to come up...") @@ -147,7 +246,7 @@ class Emulator: self.stop() return ret - def launch_process(self) -> subprocess.Popen: + def _launch_process(self) -> subprocess.Popen: args = self.make_args() env = self.make_env() @@ -180,11 +279,13 @@ class Emulator: # process is running, no need to start again return + self.start_tropic_model() + self.transport = transport or self._get_transport() - self.process = self.launch_process() + self.process = self._launch_process() _RUNNING_PIDS.add(self.process) try: - self.wait_until_ready() + self._wait_until_ready() except TimeoutError: # Assuming that after the default 60-second timeout, the process is stuck LOG.warning(f"Emulator did not come up after {EMULATOR_WAIT_TIME} seconds") @@ -223,6 +324,8 @@ class Emulator: self.process.kill() _RUNNING_PIDS.remove(self.process) + self.stop_tropic_model() + _rm_f(self.profile_dir / "trezor.pid") _rm_f(self.profile_dir / "trezor.port") self.process = None @@ -255,6 +358,10 @@ class CoreEmulator(Emulator): def __init__( self, *args: Any, + launch_tropic_model: bool = False, + tropic_model_port: Optional[int] = None, + tropic_model_configfile: Optional[str] = None, + tropic_model_logfile: Union[TextIO, str, Path, None] = None, port: Optional[int] = None, main_args: Sequence[str] = ("-m", "main"), workdir: Optional[Path] = None, @@ -271,12 +378,34 @@ class CoreEmulator(Emulator): if sdcard is not None: self.sdcard.write_bytes(sdcard) + if launch_tropic_model: + assert tropic_model_port + assert tropic_model_configfile + self.tropic_model = TropicModel( + workdir=self.workdir, + port=tropic_model_port, + configfile=tropic_model_configfile, + logfile=( + tropic_model_logfile or self.profile_dir / "trezor-tropic-model.log" + ), + ) + else: + self.tropic_model = None + if port: self.port = port self.disable_animation = disable_animation self.main_args = list(main_args) self.heap_size = heap_size + def start_tropic_model(self) -> None: + if self.tropic_model: + self.tropic_model.start() + + def stop_tropic_model(self) -> None: + if self.tropic_model: + self.tropic_model.stop() + def make_env(self) -> Dict[str, str]: env = super().make_env() env.update( @@ -289,6 +418,8 @@ class CoreEmulator(Emulator): if self.headless or self.disable_animation: env["TREZOR_DISABLE_FADE"] = "1" env["TREZOR_DISABLE_ANIMATION"] = "1" + if self.tropic_model: + env["TROPIC_MODEL_PORT"] = str(self.tropic_model.port) return env @@ -309,3 +440,9 @@ class LegacyEmulator(Emulator): if self.headless: env["SDL_VIDEODRIVER"] = "dummy" return env + + def start_tropic_model(self) -> None: + pass + + def stop_tropic_model(self) -> None: + pass diff --git a/tests/conftest.py b/tests/conftest.py index fd7963899c..027bcf82e1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -130,6 +130,7 @@ def emulator(request: pytest.FixtureRequest) -> t.Generator["Emulator", None, No headless=True, auto_interact=not interact, main_args=_emulator_wrapper_main_args(), + launch_tropic_model=True, ) as emu: yield emu diff --git a/tests/emulators.py b/tests/emulators.py index b3573abf5e..90eecbc54c 100644 --- a/tests/emulators.py +++ b/tests/emulators.py @@ -34,6 +34,8 @@ CORE_SRC_DIR = ROOT / "core" / "src" ENV = {"SDL_VIDEODRIVER": "dummy"} +TROPIC_MODEL_CONFIGFILE = ROOT / "tests" / "tropic_model" / "config.yml" + def check_version(tag: str, version_tuple: Tuple[int, int, int]) -> None: if tag is not None and tag.startswith("v") and len(tag.split(".")) == 3: @@ -67,6 +69,14 @@ def get_tags() -> Dict[str, List[str]]: ALL_TAGS = get_tags() +def _get_tropic_model_port(worker_id: int) -> int: + """Get a unique port for this worker process' Tropic model. + + Guarantees to be unique because each worker has a unique ID. + """ + return 28992 + worker_id # 28992 is the default port tvl server listens to + + def _get_port(worker_id: int) -> int: """Get a unique port for this worker process on which it can run. @@ -88,6 +98,7 @@ class EmulatorWrapper: headless: bool = True, auto_interact: bool = True, main_args: Sequence[str] = ("-m", "main"), + launch_tropic_model: bool = False, ) -> None: if tag is not None: executable = filename_from_tag(gen, tag) @@ -105,8 +116,12 @@ class EmulatorWrapper: logs_dir = os.environ.get("TREZOR_PYTEST_LOGS_DIR") logfile = None + tropic_model_logfile = None if logs_dir: logfile = Path(logs_dir) / f"trezor-{worker_id}.log" + tropic_model_logfile = ( + Path(logs_dir) / f"trezor-tropic-model-{worker_id}.log" + ) if gen == "legacy": self.emulator = LegacyEmulator( @@ -123,6 +138,10 @@ class EmulatorWrapper: self.profile_dir.name, storage=storage, workdir=workdir, + launch_tropic_model=launch_tropic_model, + tropic_model_port=_get_tropic_model_port(worker_id), + tropic_model_configfile=str(TROPIC_MODEL_CONFIGFILE), + tropic_model_logfile=tropic_model_logfile, port=_get_port(worker_id), headless=headless, auto_interact=auto_interact,