2020-01-15 15:53:50 +00:00
#!/usr/bin/env python3
2022-05-25 10:56:02 +00:00
from __future__ import annotations
2020-02-12 10:18:58 +00:00
import logging
2020-01-15 15:53:50 +00:00
import os
import platform
import signal
import subprocess
import sys
import tempfile
from pathlib import Path
2022-10-10 08:41:35 +00:00
from typing import Optional , TextIO
2020-01-15 15:53:50 +00:00
import click
import trezorlib . debuglink
import trezorlib . device
from trezorlib . _internal . emulator import CoreEmulator
try :
import inotify . adapters
2020-02-17 11:33:38 +00:00
except Exception :
2020-01-15 15:53:50 +00:00
inotify = None
2022-01-28 15:02:17 +00:00
HERE = Path ( __file__ ) . resolve ( ) . parent
2020-07-27 14:59:51 +00:00
MICROPYTHON = HERE / " build " / " unix " / " trezor-emu-core "
2020-01-15 15:53:50 +00:00
SRC_DIR = HERE / " src "
PROFILING_WRAPPER = HERE / " prof " / " prof.py "
PROFILE_BASE = Path . home ( ) / " .trezoremu "
2021-04-07 09:09:18 +00:00
TREZOR_STORAGE_FILES = (
" trezor.flash " ,
" trezor.sdcard " ,
)
2020-01-15 15:53:50 +00:00
2022-05-25 10:56:02 +00:00
def run_command_with_emulator ( emulator : CoreEmulator , command : list [ str ] ) - > int :
2020-01-15 15:53:50 +00:00
with emulator :
# first start the subprocess
process = subprocess . Popen ( command )
# After the subprocess is started, ignore SIGINT in parent
# (so that we don't need to handle KeyboardInterrupts)
signal . signal ( signal . SIGINT , signal . SIG_IGN )
# SIGINTs will be sent to all children by the OS, so we should be able to safely
# wait for their exit.
return process . wait ( )
2022-05-25 10:56:02 +00:00
def run_emulator ( emulator : CoreEmulator ) - > int :
2020-01-15 15:53:50 +00:00
with emulator :
signal . signal ( signal . SIGINT , signal . SIG_IGN )
return emulator . wait ( )
2022-05-25 10:56:02 +00:00
def watch_emulator ( emulator : CoreEmulator ) - > int :
assert inotify is not None
2020-01-15 15:53:50 +00:00
watch = inotify . adapters . InotifyTree ( str ( SRC_DIR ) )
try :
for _ , type_names , _ , _ in watch . event_gen ( yield_nones = False ) :
if " IN_CLOSE_WRITE " in type_names :
emulator . restart ( )
except KeyboardInterrupt :
emulator . stop ( )
return 0
2024-10-23 19:44:42 +00:00
def run_debugger ( emulator : CoreEmulator , gdb_script_file : str | Path | None , valgrind : bool = False , run_command : list [ str ] = [ ] ) - > None :
2020-01-15 15:53:50 +00:00
os . chdir ( emulator . workdir )
env = emulator . make_env ( )
2024-10-23 19:44:42 +00:00
if valgrind :
dbg_command = [ " valgrind " , " -v " , " --tool=callgrind " , " --read-inline-info=yes " , str ( emulator . executable ) ] + emulator . make_args ( )
elif platform . system ( ) == " Darwin " :
2020-01-15 15:53:50 +00:00
env [ " PATH " ] = " /usr/bin "
2024-10-23 19:44:42 +00:00
dbg_command = [ " lldb " , " -f " , str ( emulator . executable ) , " -- " ] + emulator . make_args ( )
2020-01-15 15:53:50 +00:00
else :
2023-05-04 12:28:57 +00:00
# Optionally run a gdb script from a file
if gdb_script_file is None :
2024-10-23 19:44:42 +00:00
dbg_command = [ " gdb " ]
2023-05-04 12:28:57 +00:00
else :
2024-10-23 19:44:42 +00:00
dbg_command = [ " gdb " , " -x " , str ( HERE / gdb_script_file ) ]
dbg_command + = [ " --args " , str ( emulator . executable ) ]
dbg_command + = emulator . make_args ( )
if not run_command :
os . execvpe ( dbg_command [ 0 ] , dbg_command , env )
else :
dbg_process = subprocess . Popen ( dbg_command , env = env )
run_process = subprocess . Popen ( run_command , env = env , shell = True )
rc = run_process . wait ( )
dbg_process . send_signal ( signal . SIGINT )
sys . exit ( rc )
2020-01-15 15:53:50 +00:00
2022-05-25 10:56:02 +00:00
def _from_env ( name : str ) - > bool :
emu: fix flag options with defaults
Click REALLY INSISTS you provide on/off switches for your options.
You can use is_flag, but then the presence of the option changes based
on the default value.
Which makes sense, really:
@option("-f", "foobar", is_flag=True, default=False)
you would expect `./cli -f` to have `foobar is True`
whereas with
@option("-f", "foobar", is_flag=True, default=True)
you would expect `./cli -f` to have `foobar is False`, otherwise it's a
no-op
this becomes fun with `default=os.environ.get("SOMETHING")`, because
then the effect of the option CHANGES with a value of environment
variable!
there's two ways around this:
a) don't use defaults, update the flag explicitly, like:
foobar = foobar or os.environ.get("FOOBAR") == "1"
b) forget about is_flag and specify an on/off switch, where the default
value works as intended
since the latter is also technically speaking more correct, i'm doing it
2020-02-12 11:31:37 +00:00
return os . environ . get ( name ) == " 1 "
2020-04-16 09:24:15 +00:00
@click.command (
context_settings = dict ( ignore_unknown_options = True , allow_interspersed_args = False )
)
2020-01-15 15:53:50 +00:00
# fmt: off
emu: fix flag options with defaults
Click REALLY INSISTS you provide on/off switches for your options.
You can use is_flag, but then the presence of the option changes based
on the default value.
Which makes sense, really:
@option("-f", "foobar", is_flag=True, default=False)
you would expect `./cli -f` to have `foobar is True`
whereas with
@option("-f", "foobar", is_flag=True, default=True)
you would expect `./cli -f` to have `foobar is False`, otherwise it's a
no-op
this becomes fun with `default=os.environ.get("SOMETHING")`, because
then the effect of the option CHANGES with a value of environment
variable!
there's two ways around this:
a) don't use defaults, update the flag explicitly, like:
foobar = foobar or os.environ.get("FOOBAR") == "1"
b) forget about is_flag and specify an on/off switch, where the default
value works as intended
since the latter is also technically speaking more correct, i'm doing it
2020-02-12 11:31:37 +00:00
@click.option ( " -a " , " --disable-animation/--enable-animation " , default = _from_env ( " TREZOR_DISABLE_ANIMATION " ) , help = " Disable animation " )
2020-01-15 15:53:50 +00:00
@click.option ( " -c " , " --command " , " run_command " , is_flag = True , help = " Run command while emulator is running " )
emu: fix flag options with defaults
Click REALLY INSISTS you provide on/off switches for your options.
You can use is_flag, but then the presence of the option changes based
on the default value.
Which makes sense, really:
@option("-f", "foobar", is_flag=True, default=False)
you would expect `./cli -f` to have `foobar is True`
whereas with
@option("-f", "foobar", is_flag=True, default=True)
you would expect `./cli -f` to have `foobar is False`, otherwise it's a
no-op
this becomes fun with `default=os.environ.get("SOMETHING")`, because
then the effect of the option CHANGES with a value of environment
variable!
there's two ways around this:
a) don't use defaults, update the flag explicitly, like:
foobar = foobar or os.environ.get("FOOBAR") == "1"
b) forget about is_flag and specify an on/off switch, where the default
value works as intended
since the latter is also technically speaking more correct, i'm doing it
2020-02-12 11:31:37 +00:00
@click.option ( " -d " , " --production/--no-production " , default = _from_env ( " PYOPT " ) , help = " Production mode (debuglink disabled) " )
2020-01-15 15:53:50 +00:00
@click.option ( " -D " , " --debugger " , is_flag = True , help = " Run emulator in debugger (gdb/lldb) " )
2021-04-07 09:09:18 +00:00
@click.option ( " -e " , " --erase " , is_flag = True , help = " Erase profile before running " )
2020-01-15 15:53:50 +00:00
@click.option ( " --executable " , type = click . Path ( exists = True , dir_okay = False ) , default = os . environ . get ( " MICROPYTHON " ) , help = " Alternate emulator executable " )
emu: fix flag options with defaults
Click REALLY INSISTS you provide on/off switches for your options.
You can use is_flag, but then the presence of the option changes based
on the default value.
Which makes sense, really:
@option("-f", "foobar", is_flag=True, default=False)
you would expect `./cli -f` to have `foobar is True`
whereas with
@option("-f", "foobar", is_flag=True, default=True)
you would expect `./cli -f` to have `foobar is False`, otherwise it's a
no-op
this becomes fun with `default=os.environ.get("SOMETHING")`, because
then the effect of the option CHANGES with a value of environment
variable!
there's two ways around this:
a) don't use defaults, update the flag explicitly, like:
foobar = foobar or os.environ.get("FOOBAR") == "1"
b) forget about is_flag and specify an on/off switch, where the default
value works as intended
since the latter is also technically speaking more correct, i'm doing it
2020-02-12 11:31:37 +00:00
@click.option ( " -g " , " --profiling/--no-profiling " , default = _from_env ( " TREZOR_PROFILING " ) , help = " Run with profiler wrapper " )
2020-10-26 19:30:55 +00:00
@click.option ( " -G " , " --alloc-profiling/--no-alloc-profiling " , default = _from_env ( " TREZOR_MEMPERF " ) , help = " Profile memory allocation (requires special micropython build) " )
2022-02-18 10:34:28 +00:00
@click.option ( " -h " , " --headless " , is_flag = True , help = " Headless mode (no display, disables animation) " )
2020-01-15 15:53:50 +00:00
@click.option ( " --heap-size " , metavar = " SIZE " , default = " 20M " , help = " Configure heap size " )
2024-09-25 16:46:20 +00:00
@click.option ( " -m " , " --tropic-model " , is_flag = True , help = " Start the Tropic Square model, needed for running the Tropic tests. Needs to be installed first. " )
2020-01-15 15:53:50 +00:00
@click.option ( " --main " , help = " Path to python main file " )
@click.option ( " --mnemonic " , " mnemonics " , multiple = True , help = " Initialize device with given mnemonic. Specify multiple times for Shamir shares. " )
emu: fix flag options with defaults
Click REALLY INSISTS you provide on/off switches for your options.
You can use is_flag, but then the presence of the option changes based
on the default value.
Which makes sense, really:
@option("-f", "foobar", is_flag=True, default=False)
you would expect `./cli -f` to have `foobar is True`
whereas with
@option("-f", "foobar", is_flag=True, default=True)
you would expect `./cli -f` to have `foobar is False`, otherwise it's a
no-op
this becomes fun with `default=os.environ.get("SOMETHING")`, because
then the effect of the option CHANGES with a value of environment
variable!
there's two ways around this:
a) don't use defaults, update the flag explicitly, like:
foobar = foobar or os.environ.get("FOOBAR") == "1"
b) forget about is_flag and specify an on/off switch, where the default
value works as intended
since the latter is also technically speaking more correct, i'm doing it
2020-02-12 11:31:37 +00:00
@click.option ( " --log-memory/--no-log-memory " , default = _from_env ( " TREZOR_LOG_MEMORY " ) , help = " Print memory usage after workflows " )
2020-01-15 15:53:50 +00:00
@click.option ( " -o " , " --output " , type = click . File ( " w " ) , default = " - " , help = " Redirect emulator output to file " )
@click.option ( " -p " , " --profile " , metavar = " NAME " , help = " Profile name or path " )
@click.option ( " -P " , " --port " , metavar = " PORT " , type = int , default = int ( os . environ . get ( " TREZOR_UDP_PORT " , 0 ) ) or None , help = " UDP port number " )
@click.option ( " -q " , " --quiet " , is_flag = True , help = " Silence emulator output " )
2022-10-10 08:41:35 +00:00
@click.option ( " -r " , " --record-dir " , help = " Directory where to record screen changes " )
2020-01-15 15:53:50 +00:00
@click.option ( " -s " , " --slip0014 " , is_flag = True , help = " Initialize device with SLIP-14 seed (all all all...) " )
2023-05-04 12:28:57 +00:00
@click.option ( " -S " , " --script-gdb-file " , type = click . Path ( exists = True , dir_okay = False ) , help = " Run gdb with an init file " )
2024-10-23 19:44:42 +00:00
@click.option ( " -V " , " --valgrind " , is_flag = True , help = " Use valgrind instead of debugger (-D) " )
2020-01-15 15:53:50 +00:00
@click.option ( " -t " , " --temporary-profile " , is_flag = True , help = " Create an empty temporary profile " )
@click.option ( " -w " , " --watch " , is_flag = True , help = " Restart emulator if sources change " )
@click.option ( " -X " , " --extra-arg " , " extra_args " , multiple = True , help = " Extra argument to pass to micropython " )
# fmt: on
@click.argument ( " command " , nargs = - 1 , type = click . UNPROCESSED )
def cli (
2022-05-25 10:56:02 +00:00
disable_animation : bool ,
run_command : bool ,
production : bool ,
debugger : bool ,
erase : bool ,
executable : str | Path ,
profiling : bool ,
alloc_profiling : bool ,
headless : bool ,
heap_size : str ,
2024-09-25 16:46:20 +00:00
tropic_model : bool ,
2022-05-25 10:56:02 +00:00
main : str ,
mnemonics : list [ str ] ,
log_memory : bool ,
profile : str ,
port : int ,
output : TextIO | None ,
quiet : bool ,
2022-10-10 08:41:35 +00:00
record_dir : Optional [ str ] ,
2022-05-25 10:56:02 +00:00
slip0014 : bool ,
2023-05-04 12:28:57 +00:00
script_gdb_file : str | Path | None ,
2024-10-23 19:44:42 +00:00
valgrind : bool ,
2022-05-25 10:56:02 +00:00
temporary_profile : bool ,
watch : bool ,
extra_args : list [ str ] ,
command : list [ str ] ,
2020-01-15 15:53:50 +00:00
) :
""" Run the trezor-core emulator.
If - c is specified , extra arguments are treated as a command that is executed with
the running emulator . This command can access the following environment variables :
2020-11-11 13:43:09 +00:00
2020-01-15 15:53:50 +00:00
\b
TREZOR_PROFILE_DIR - path to storage directory
TREZOR_PATH - trezorlib connection string
TREZOR_UDP_PORT - UDP port on which the emulator listens
TREZOR_FIDO2_UDP_PORT - UDP port for FIDO2
By default , emulator output goes to stdout . If silenced with - q , it is redirected
to $ TREZOR_PROFILE_DIR / trezor . log . You can also specify a custom path with - o .
"""
if executable :
executable = Path ( executable )
else :
executable = MICROPYTHON
if command and not run_command :
raise click . ClickException ( " Extra arguments found. Did you mean to use -c? " )
2024-09-25 16:46:20 +00:00
if watch and ( command or debugger or tropic_model ) :
2020-01-30 14:47:11 +00:00
raise click . ClickException ( " Cannot use -w together with -c or -D " )
2020-01-15 15:53:50 +00:00
if watch and inotify is None :
raise click . ClickException ( " inotify module is missing, install with pip " )
2020-10-26 19:30:55 +00:00
if main and ( profiling or alloc_profiling ) :
2020-01-15 15:53:50 +00:00
raise click . ClickException ( " Cannot use --main and -g together " )
if slip0014 and mnemonics :
raise click . ClickException ( " Cannot use -s and --mnemonic together " )
if slip0014 :
mnemonics = [ " " . join ( [ " all " ] * 12 ) ]
if mnemonics and debugger :
raise click . ClickException ( " Cannot load mnemonics when running in debugger " )
if mnemonics and production :
raise click . ClickException ( " Cannot load mnemonics in production mode " )
2020-10-26 19:30:55 +00:00
if profiling or alloc_profiling :
2020-01-15 15:53:50 +00:00
main_args = [ str ( PROFILING_WRAPPER ) ]
elif main :
main_args = [ main ]
else :
main_args = [ " -m " , " main " ]
if profile and temporary_profile :
raise click . ClickException ( " Cannot use -p and -t together " )
tempdir = None
if profile :
if " / " in profile :
profile_dir = Path ( profile )
else :
profile_dir = PROFILE_BASE / profile
elif temporary_profile :
tempdir = tempfile . TemporaryDirectory ( prefix = " trezor-emulator- " )
profile_dir = Path ( tempdir . name )
elif " TREZOR_PROFILE_DIR " in os . environ :
profile_dir = Path ( os . environ [ " TREZOR_PROFILE_DIR " ] )
else :
profile_dir = Path ( " /var/tmp " )
2021-04-07 09:09:18 +00:00
if erase :
for entry in TREZOR_STORAGE_FILES :
( profile_dir / entry ) . unlink ( missing_ok = True )
2020-01-15 15:53:50 +00:00
if quiet :
output = None
2020-02-12 10:18:58 +00:00
logger = logging . getLogger ( " trezorlib._internal.emulator " )
logger . setLevel ( logging . INFO )
logger . addHandler ( logging . StreamHandler ( ) )
2020-01-15 15:53:50 +00:00
emulator = CoreEmulator (
executable ,
profile_dir ,
logfile = output ,
port = port ,
headless = headless ,
debug = not production ,
extra_args = extra_args ,
main_args = main_args ,
heap_size = heap_size ,
disable_animation = disable_animation ,
workdir = SRC_DIR ,
)
emulator_env = dict (
TREZOR_PATH = f " udp:127.0.0.1: { emulator . port } " ,
TREZOR_PROFILE_DIR = str ( profile_dir . resolve ( ) ) ,
TREZOR_UDP_PORT = str ( emulator . port ) ,
TREZOR_FIDO2_UDP_PORT = str ( emulator . port + 2 ) ,
2020-01-21 13:25:54 +00:00
TREZOR_SRC = str ( SRC_DIR ) ,
2020-01-15 15:53:50 +00:00
)
os . environ . update ( emulator_env )
for k , v in emulator_env . items ( ) :
click . echo ( f " { k } = { v } " )
if log_memory :
os . environ [ " TREZOR_LOG_MEMORY " ] = " 1 "
2020-10-26 19:30:55 +00:00
if alloc_profiling :
os . environ [ " TREZOR_MEMPERF " ] = " 1 "
2024-10-23 19:44:42 +00:00
if debugger or valgrind :
run_debugger ( emulator , script_gdb_file , valgrind , command )
2020-01-15 15:53:50 +00:00
raise RuntimeError ( " run_debugger should not return " )
emulator . start ( )
if mnemonics :
if slip0014 :
label = " SLIP-0014 "
elif profile :
label = profile_dir . name
else :
label = " Emulator "
2022-05-25 10:56:02 +00:00
assert emulator . client is not None
2020-01-15 15:53:50 +00:00
trezorlib . device . wipe ( emulator . client )
trezorlib . debuglink . load_device (
emulator . client ,
mnemonics ,
pin = None ,
passphrase_protection = False ,
label = label ,
)
2022-10-10 08:41:35 +00:00
if record_dir :
assert emulator . client is not None
trezorlib . debuglink . record_screen (
emulator . client , record_dir , report_func = print
)
2024-09-25 16:46:20 +00:00
if tropic_model :
run_command = True
command = " ./tropic-model.sh "
2020-01-15 15:53:50 +00:00
if run_command :
ret = run_command_with_emulator ( emulator , command )
elif watch :
ret = watch_emulator ( emulator )
else :
ret = run_emulator ( emulator )
if tempdir is not None :
tempdir . cleanup ( )
sys . exit ( ret )
if __name__ == " __main__ " :
cli ( )