diff --git a/chia/_tests/cmds/test_daemon.py b/chia/_tests/cmds/test_daemon.py new file mode 100644 index 000000000000..a7d50ffa91ec --- /dev/null +++ b/chia/_tests/cmds/test_daemon.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any, Dict, Optional + +import pytest +from _pytest.capture import CaptureFixture +from click.testing import CliRunner +from pytest_mock import MockerFixture + +from chia.cmds.chia import cli +from chia.cmds.start_funcs import create_start_daemon_connection + + +@pytest.mark.anyio +@pytest.mark.parametrize("skip_keyring", [False, True]) +@pytest.mark.parametrize("unlock_keyring", [False, True]) +async def test_daemon( + skip_keyring: bool, unlock_keyring: bool, mocker: MockerFixture, capsys: CaptureFixture[str] +) -> None: + class DummyConnection: + @staticmethod + async def is_keyring_locked() -> bool: + return unlock_keyring + + @staticmethod + async def unlock_keyring(_passphrase: str) -> bool: + return True + + async def connect_to_daemon_and_validate(_root_path: Path, _config: Dict[str, Any]) -> DummyConnection: + return DummyConnection() + + class DummyKeychain: + @staticmethod + def get_cached_master_passphrase() -> Optional[str]: + return None + + def get_current_passphrase() -> Optional[str]: + return "a-passphrase" + + mocker.patch("chia.cmds.start_funcs.connect_to_daemon_and_validate", side_effect=connect_to_daemon_and_validate) + mocker.patch("chia.cmds.start_funcs.Keychain", new=DummyKeychain) + mocker.patch("chia.cmds.start_funcs.get_current_passphrase", side_effect=get_current_passphrase) + + daemon = await create_start_daemon_connection(Path("/path-not-exist"), {}, skip_keyring=skip_keyring) + assert daemon is not None + captured = capsys.readouterr() + assert captured.err == "" + if skip_keyring: + assert captured.out.endswith("Skipping to unlock keyring\n") + else: + assert not captured.out.endswith("Skipping to unlock keyring\n") + + +def test_start_daemon(tmp_path: Path, empty_keyring: Any, mocker: MockerFixture) -> None: + class DummyDaemon: + @staticmethod + async def close() -> None: + return None + + async def create_start_daemon_connection_dummy( + root_path: Path, config: Dict[str, Any], *, skip_keyring: bool + ) -> DummyDaemon: + return DummyDaemon() + + mocker.patch( + "chia.cmds.start_funcs.create_start_daemon_connection", side_effect=create_start_daemon_connection_dummy + ) + + runner = CliRunner() + result = runner.invoke( + cli, + ["--root-path", str(tmp_path), "init"], + ) + assert result.exit_code == 0 + result = runner.invoke(cli, ["--root-path", str(tmp_path), "start", "daemon", "-s"]) + assert result.exit_code == 0 diff --git a/chia/cmds/sim_funcs.py b/chia/cmds/sim_funcs.py index c9207fc2975d..698d8ef42353 100644 --- a/chia/cmds/sim_funcs.py +++ b/chia/cmds/sim_funcs.py @@ -291,7 +291,7 @@ async def async_config_wizard( print("Starting Simulator now...\n\n") sys.argv[0] = str(Path(sys.executable).parent / "chia") # fix path for tests - await async_start(root_path, config, ("simulator",), False) + await async_start(root_path, config, ("simulator",), restart=False, skip_keyring=False) # now we make sure the simulator has a genesis block print("Please wait, generating genesis block.") diff --git a/chia/cmds/start.py b/chia/cmds/start.py index e6c443533d2a..1e63d53be296 100644 --- a/chia/cmds/start.py +++ b/chia/cmds/start.py @@ -8,9 +8,10 @@ @click.command("start", help="Start service groups") @click.option("-r", "--restart", is_flag=True, type=bool, help="Restart running services") +@click.option("-s", "--skip-keyring", is_flag=True, type=bool, help="Skip to unlock keyring") @click.argument("group", type=click.Choice(list(all_groups())), nargs=-1, required=True) @click.pass_context -def start_cmd(ctx: click.Context, restart: bool, group: tuple[str, ...]) -> None: +def start_cmd(ctx: click.Context, restart: bool, skip_keyring: bool, group: tuple[str, ...]) -> None: import asyncio from chia.cmds.beta_funcs import warn_if_beta_enabled @@ -20,4 +21,4 @@ def start_cmd(ctx: click.Context, restart: bool, group: tuple[str, ...]) -> None root_path = ctx.obj["root_path"] config = load_config(root_path, "config.yaml") warn_if_beta_enabled(config) - asyncio.run(async_start(root_path, config, group, restart)) + asyncio.run(async_start(root_path, config, group, restart, skip_keyring=skip_keyring)) diff --git a/chia/cmds/start_funcs.py b/chia/cmds/start_funcs.py index cc940b6a3315..0857f7e30719 100644 --- a/chia/cmds/start_funcs.py +++ b/chia/cmds/start_funcs.py @@ -31,7 +31,9 @@ def launch_start_daemon(root_path: Path) -> subprocess.Popen: return process -async def create_start_daemon_connection(root_path: Path, config: Dict[str, Any]) -> Optional[DaemonProxy]: +async def create_start_daemon_connection( + root_path: Path, config: Dict[str, Any], *, skip_keyring: bool +) -> Optional[DaemonProxy]: connection = await connect_to_daemon_and_validate(root_path, config) if connection is None: print("Starting daemon") @@ -44,24 +46,29 @@ async def create_start_daemon_connection(root_path: Path, config: Dict[str, Any] # it prints "daemon: listening" connection = await connect_to_daemon_and_validate(root_path, config) if connection: - passphrase = None - if await connection.is_keyring_locked(): - passphrase = Keychain.get_cached_master_passphrase() - if passphrase is None or not Keychain.master_passphrase_is_valid(passphrase): - with ThreadPoolExecutor(max_workers=1, thread_name_prefix="get_current_passphrase") as executor: - passphrase = await asyncio.get_running_loop().run_in_executor(executor, get_current_passphrase) + if skip_keyring: + print("Skipping to unlock keyring") + else: + passphrase = None + if await connection.is_keyring_locked(): + passphrase = Keychain.get_cached_master_passphrase() + if passphrase is None or not Keychain.master_passphrase_is_valid(passphrase): + with ThreadPoolExecutor(max_workers=1, thread_name_prefix="get_current_passphrase") as executor: + passphrase = await asyncio.get_running_loop().run_in_executor(executor, get_current_passphrase) - if passphrase: - print("Unlocking daemon keyring") - await connection.unlock_keyring(passphrase) + if passphrase: + print("Unlocking daemon keyring") + await connection.unlock_keyring(passphrase) return connection return None -async def async_start(root_path: Path, config: Dict[str, Any], group: tuple[str, ...], restart: bool) -> None: +async def async_start( + root_path: Path, config: Dict[str, Any], group: tuple[str, ...], restart: bool, *, skip_keyring: bool +) -> None: try: - daemon = await create_start_daemon_connection(root_path, config) + daemon = await create_start_daemon_connection(root_path, config, skip_keyring=skip_keyring) except KeychainMaxUnlockAttempts: print("Failed to unlock keyring") return None