Marco Ricci commited on 2026-01-18 17:35:34
Zeige 2 geänderte Dateien mit 33 Einfügungen und 19 Löschungen.
Generally, if we have an abstract socket connected to an SSH agent, then this is sufficient for us to construct a client based on the socket, and interact with the agent as needed. However, in the CLI tests, there are situations where we are orchestrating the whole `derivepassphrase` application, and the application must connect to the agent itself. For these situations, we need to set up the necessary configuration to pass the agent's socket address to `derivepassphrase`. For agents merely interfaced in the test suite, we were not doing such a setup, causing orchestrated `derivepassphrase` application instances to believe there was no agent to connect to. Fix this by (a) exposing the socket address on the socket object, internal to the spawning/interfacing function, and (b) let interfacing functions return a pair of socket and socket address, not just the socket. (We intend to keep the interface for actually obtaining the address private to each interfacing function, because not all socket objects can easily be retrofitted with extra methods to query the socket address.) (As a side effect, give the socket providers on The Annoying OS more specific typing, so that the type checker can verify that the interfacing functions are accessing the correct attributes and methods for obtaining the socket address.)
| ... | ... |
@@ -429,6 +429,7 @@ class WindowsNamedPipeHandle: |
| 429 | 429 |
if getattr(exc, "winerror", None) == ERROR_PIPE_BUSY: |
| 430 | 430 |
continue # retry |
| 431 | 431 |
raise |
| 432 |
+ self._name = name |
|
| 432 | 433 |
|
| 433 | 434 |
def __enter__(self) -> Self: # pragma: unless the-annoying-os no cover |
| 434 | 435 |
return self |
| ... | ... |
@@ -521,6 +522,10 @@ class WindowsNamedPipeHandle: |
| 521 | 522 |
finally: |
| 522 | 523 |
CloseHandle(wait_event) |
| 523 | 524 |
|
| 525 |
+ def named_pipe_name(self) -> str: |
|
| 526 |
+ """Return the named pipe name this socket is connected to.""" |
|
| 527 |
+ return self._name |
|
| 528 |
+ |
|
| 524 | 529 |
@classmethod |
| 525 | 530 |
def for_openssh(cls) -> Self: # pragma: unless the-annoying-os no cover |
| 526 | 531 |
"""Construct a named pipe for use with OpenSSH on The Annoying OS. |
| ... | ... |
@@ -653,7 +658,7 @@ class SocketProvider: |
| 653 | 658 |
@staticmethod |
| 654 | 659 |
def _windows_named_pipe( |
| 655 | 660 |
pipe_name: _WindowsNamedPipeSocketAddress | str | None, |
| 656 |
- ) -> _types.SSHAgentSocket: |
|
| 661 |
+ ) -> WindowsNamedPipeHandle: |
|
| 657 | 662 |
r"""Return a socket wrapper around a Windows named pipe. |
| 658 | 663 |
|
| 659 | 664 |
Args: |
| ... | ... |
@@ -699,7 +704,7 @@ class SocketProvider: |
| 699 | 704 |
return WindowsNamedPipeHandle(pipe_name) |
| 700 | 705 |
|
| 701 | 706 |
@classmethod |
| 702 |
- def windows_named_pipe_for_pageant(cls) -> _types.SSHAgentSocket: |
|
| 707 |
+ def windows_named_pipe_for_pageant(cls) -> WindowsNamedPipeHandle: |
|
| 703 | 708 |
"""Return a socket wrapper connected to Pageant on The Annoying OS. |
| 704 | 709 |
|
| 705 | 710 |
Raises: |
| ... | ... |
@@ -713,7 +718,7 @@ class SocketProvider: |
| 713 | 718 |
return cls._windows_named_pipe(_WindowsNamedPipeSocketAddress.PAGEANT) |
| 714 | 719 |
|
| 715 | 720 |
@classmethod |
| 716 |
- def windows_named_pipe_for_openssh(cls) -> _types.SSHAgentSocket: |
|
| 721 |
+ def windows_named_pipe_for_openssh(cls) -> WindowsNamedPipeHandle: |
|
| 717 | 722 |
"""Return a socket wrapper connected to the OpenSSH agent on The Annoying OS. |
| 718 | 723 |
|
| 719 | 724 |
Raises: |
| ... | ... |
@@ -728,7 +733,7 @@ class SocketProvider: |
| 728 | 733 |
return cls._windows_named_pipe(_WindowsNamedPipeSocketAddress.OPENSSH) |
| 729 | 734 |
|
| 730 | 735 |
@classmethod |
| 731 |
- def windows_named_pipe_ssh_auth_sock(cls) -> _types.SSHAgentSocket: |
|
| 736 |
+ def windows_named_pipe_ssh_auth_sock(cls) -> WindowsNamedPipeHandle: |
|
| 732 | 737 |
r"""Return a socket wrapper connected to the agent in `SSH_AUTH_SOCK`. |
| 733 | 738 |
|
| 734 | 739 |
The `SSH_AUTH_SOCK` environment variable is assumed to contain |
| ... | ... |
@@ -110,7 +110,7 @@ class SSHAgentInterfaceFunc(Protocol): |
| 110 | 110 |
self, |
| 111 | 111 |
executable: str | None, |
| 112 | 112 |
env: dict[str, Any], |
| 113 |
- ) -> _types.SSHAgentSocket | None: |
|
| 113 |
+ ) -> tuple[_types.SSHAgentSocket, str] | None: |
|
| 114 | 114 |
"""Interface the SSH agent. |
| 115 | 115 |
|
| 116 | 116 |
Args: |
| ... | ... |
@@ -125,8 +125,9 @@ class SSHAgentInterfaceFunc(Protocol): |
| 125 | 125 |
(Ignored. Not relevant for interfacing functions.) |
| 126 | 126 |
|
| 127 | 127 |
Returns: |
| 128 |
- An abstract socket connected to the specified SSH agent, if |
|
| 129 |
- possible; otherwise, `None`. |
|
| 128 |
+ A 2-tuple, if possible, containing an abstract socket |
|
| 129 |
+ connected to the specified SSH agent and the associated |
|
| 130 |
+ address; otherwise, `None`. |
|
| 130 | 131 |
|
| 131 | 132 |
It is the caller's responsibility to clean up the socket |
| 132 | 133 |
(through use of the context manager protocol). |
| ... | ... |
@@ -257,7 +258,7 @@ def spawn_noop( # pragma: no cover [unused] |
| 257 | 258 |
def interface_pageant_on_the_annoying_os( |
| 258 | 259 |
executable: str | None, |
| 259 | 260 |
env: dict[str, Any], |
| 260 |
-) -> _types.SSHAgentSocket | None: # pragma: no cover [external] |
|
| 261 |
+) -> tuple[_types.SSHAgentSocket, str] | None: # pragma: no cover [external] |
|
| 261 | 262 |
"""Interface a Pageant instance on The Annoying OS, if possible. |
| 262 | 263 |
|
| 263 | 264 |
Args: |
| ... | ... |
@@ -274,15 +275,17 @@ def interface_pageant_on_the_annoying_os( |
| 274 | 275 |
""" |
| 275 | 276 |
del executable, env |
| 276 | 277 |
try: |
| 277 |
- return socketprovider.SocketProvider.windows_named_pipe_for_pageant() |
|
| 278 |
+ socket = socketprovider.SocketProvider.windows_named_pipe_for_pageant() |
|
| 278 | 279 |
except socketprovider.WindowsNamedPipesNotAvailableError: |
| 279 | 280 |
return None |
| 281 |
+ else: |
|
| 282 |
+ return (socket, socket.named_pipe_name()) |
|
| 280 | 283 |
|
| 281 | 284 |
|
| 282 | 285 |
def interface_openssh_agent_on_the_annoying_os( |
| 283 | 286 |
executable: str | None, |
| 284 | 287 |
env: dict[str, Any], |
| 285 |
-) -> _types.SSHAgentSocket | None: # pragma: no cover [external] |
|
| 288 |
+) -> tuple[_types.SSHAgentSocket, str] | None: # pragma: no cover [external] |
|
| 286 | 289 |
"""Interface an OpenSSH agent instance on The Annoying OS, if possible. |
| 287 | 290 |
|
| 288 | 291 |
Args: |
| ... | ... |
@@ -299,9 +302,11 @@ def interface_openssh_agent_on_the_annoying_os( |
| 299 | 302 |
""" |
| 300 | 303 |
del executable, env |
| 301 | 304 |
try: |
| 302 |
- return socketprovider.SocketProvider.windows_named_pipe_for_openssh() |
|
| 305 |
+ socket = socketprovider.SocketProvider.windows_named_pipe_for_openssh() |
|
| 303 | 306 |
except socketprovider.WindowsNamedPipesNotAvailableError: |
| 304 | 307 |
return None |
| 308 |
+ else: |
|
| 309 |
+ return (socket, socket.named_pipe_name()) |
|
| 305 | 310 |
|
| 306 | 311 |
|
| 307 | 312 |
class SpawnHandler(NamedTuple): |
| ... | ... |
@@ -541,17 +546,17 @@ def spawn_named_agent( # noqa: C901 |
| 541 | 546 |
exit_stack = contextlib.ExitStack() |
| 542 | 547 |
agent_env = os.environ.copy() |
| 543 | 548 |
ssh_auth_sock = agent_env.pop("SSH_AUTH_SOCK", None)
|
| 544 |
- proc_or_socket = spawn_func(executable=executable, env=agent_env) |
|
| 549 |
+ proc_or_socket_data = spawn_func(executable=executable, env=agent_env) |
|
| 545 | 550 |
with exit_stack: |
| 546 | 551 |
if agent_type == data.KnownSSHAgent.StubbedSSHAgent: |
| 547 | 552 |
ssh_auth_sock = None |
| 548 | 553 |
elif spawn_func is spawn_noop: |
| 549 | 554 |
ssh_auth_sock = os.environ.get("SSH_AUTH_SOCK")
|
| 550 |
- elif proc_or_socket is None: # pragma: no cover [external] |
|
| 555 |
+ elif proc_or_socket_data is None: # pragma: no cover [external] |
|
| 551 | 556 |
err_msg = f"Cannot spawn or interface with usable {agent_name}"
|
| 552 | 557 |
raise CannotSpawnError(err_msg) |
| 553 |
- elif isinstance(proc_or_socket, subprocess.Popen): |
|
| 554 |
- proc = proc_or_socket |
|
| 558 |
+ elif isinstance(proc_or_socket_data, subprocess.Popen): |
|
| 559 |
+ proc = proc_or_socket_data |
|
| 555 | 560 |
exit_stack.enter_context(terminate_on_exit(proc)) |
| 556 | 561 |
assert os.environ.get("SSH_AUTH_SOCK") == startup_ssh_auth_sock, (
|
| 557 | 562 |
f"SSH_AUTH_SOCK mismatch after spawning {agent_name}"
|
| ... | ... |
@@ -572,6 +577,8 @@ def spawn_named_agent( # noqa: C901 |
| 572 | 577 |
): # pragma: no cover [external] |
| 573 | 578 |
err_msg = f"Cannot parse agent output: {pid_line!r}"
|
| 574 | 579 |
raise CannotSpawnError(err_msg) |
| 580 |
+ else: |
|
| 581 |
+ ssh_auth_sock = proc_or_socket_data[1] |
|
| 575 | 582 |
monkeypatch = exit_stack.enter_context(pytest.MonkeyPatch.context()) |
| 576 | 583 |
if agent_type == data.KnownSSHAgent.StubbedSSHAgent: |
| 577 | 584 |
assert ssh_auth_sock is None |
| ... | ... |
@@ -596,10 +603,12 @@ def spawn_named_agent( # noqa: C901 |
| 596 | 603 |
) |
| 597 | 604 |
) |
| 598 | 605 |
elif ( |
| 599 |
- not isinstance(proc_or_socket, subprocess.Popen) |
|
| 600 |
- and proc_or_socket is not None |
|
| 606 |
+ not isinstance(proc_or_socket_data, subprocess.Popen) |
|
| 607 |
+ and proc_or_socket_data is not None |
|
| 601 | 608 |
): |
| 602 |
- socket: _types.SSHAgentSocket = proc_or_socket |
|
| 609 |
+ socket: _types.SSHAgentSocket |
|
| 610 |
+ socket, ssh_auth_sock = proc_or_socket_data |
|
| 611 |
+ monkeypatch.setenv("SSH_AUTH_SOCK", ssh_auth_sock)
|
|
| 603 | 612 |
client = exit_stack.enter_context( |
| 604 | 613 |
ssh_agent.SSHAgentClient.ensure_agent_subcontext(conn=socket) |
| 605 | 614 |
) |
| ... | ... |
@@ -635,7 +644,7 @@ def spawn_named_agent( # noqa: C901 |
| 635 | 644 |
agent_type, |
| 636 | 645 |
client, |
| 637 | 646 |
spawn_func is not spawn_noop |
| 638 |
- if isinstance(proc_or_socket, subprocess.Popen) |
|
| 647 |
+ if isinstance(proc_or_socket_data, subprocess.Popen) |
|
| 639 | 648 |
else False, |
| 640 | 649 |
) |
| 641 | 650 |
assert os.environ.get("SSH_AUTH_SOCK", None) == startup_ssh_auth_sock, (
|
| 642 | 651 |