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
|
|
|
|
|
|
|
|
|
2023-05-04 12:28:57 +00:00
|
|
|
def run_debugger(emulator: CoreEmulator, gdb_script_file: str | Path | None) -> None:
|
2020-01-15 15:53:50 +00:00
|
|
|
os.chdir(emulator.workdir)
|
|
|
|
env = emulator.make_env()
|
|
|
|
if platform.system() == "Darwin":
|
|
|
|
env["PATH"] = "/usr/bin"
|
|
|
|
os.execvpe(
|
|
|
|
"lldb",
|
2022-05-25 10:56:02 +00:00
|
|
|
["lldb", "-f", str(emulator.executable), "--"] + emulator.make_args(),
|
2020-01-15 15:53:50 +00:00
|
|
|
env,
|
|
|
|
)
|
|
|
|
else:
|
2023-05-04 12:28:57 +00:00
|
|
|
# Optionally run a gdb script from a file
|
|
|
|
if gdb_script_file is None:
|
|
|
|
gdb = ["gdb"]
|
|
|
|
else:
|
|
|
|
gdb = ["gdb", "-x", str(HERE / gdb_script_file)]
|
2020-01-15 15:53:50 +00:00
|
|
|
os.execvpe(
|
2023-05-04 12:28:57 +00:00
|
|
|
"gdb",
|
|
|
|
gdb + ["--args", str(emulator.executable)] + emulator.make_args(),
|
|
|
|
env,
|
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")
|
|
|
|
@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")
|
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,
|
|
|
|
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,
|
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?")
|
|
|
|
|
2020-01-30 14:47:11 +00:00
|
|
|
if watch and (command or debugger):
|
|
|
|
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"
|
|
|
|
|
2020-01-15 15:53:50 +00:00
|
|
|
if debugger:
|
2023-05-04 12:28:57 +00:00
|
|
|
run_debugger(emulator, script_gdb_file)
|
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
|
|
|
|
)
|
|
|
|
|
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()
|