Skip to content

Command descriptors

reemote.commands defines the base Command, the raw StringCommand, the TransferCommand base for descriptors that copy bytes over the connection instead of running shell text, and the WaitCommand base for descriptors that poll the host until a condition holds; the concrete typed descriptors live beside the operations that yield them (reemote.modules.dnf.dnf, reemote.modules.files.files, reemote.modules.server.server). They are dumb, frozen value objects: an operation never executes anything, it only describes what would have to run. The raw StringCommand carries shell text verbatim — its shell() returns the command unchanged, so it runs (and takes the doas/sudo/su prefixes) exactly like a typed shell descriptor; it is the vehicle for a demo-side operation running a one-off command such as the sysctl step of the sysctl recipe without a library operation. The typed descriptors exist so the test framework's SimulatedHost (in tests/simulated_host.py) can dispatch on them in its execute_command and apply the equivalent mutation; the library's send_command forwards to that seam. On the real transport a TransferCommand is dispatched on its base — send_command hands the host to its transfer, which copies over the connection's own protocol (asyncssh's scp) rather than through a shell, so the doas/sudo/su escalation flags do not apply to it. A WaitCommand is dispatched the same way — send_command awaits its wait, which polls the host until its condition holds or raises WaitTimeoutError — and is likewise unprivileged; see WaitCommand here and the server.wait and server.wait_service operations that yield the concrete poll (both through the generic WaitForFactCommand, which polls a fact class until a module-level predicate holds).

Reference

reemote.commands

Command descriptors (design doc section 4.5).

Operations yield command descriptors, not strings, so the driver can distinguish command types and handle them appropriately. Descriptors stay dumb; the driver maps them onto host actions via send_command.

This module holds the base Command, the raw StringCommand, the TransferCommand base for descriptors that copy bytes over the connection instead of running shell text, and the WaitCommand base for descriptors that poll the host until a condition holds; concrete typed descriptors live beside the operations that yield them (modules.dnf.dnf, modules.files.files, modules.server.server).

Command dataclass

Command()

Base class for all command descriptors.

StringCommand dataclass

StringCommand()

A raw shell command (would be sent over SSH in a real implementation).

TransferCommand dataclass

TransferCommand()

Base for descriptors that transfer bytes rather than run shell text.

A transfer has no shell() rendering -- it is not executed through the shell pipeline -- so send_command dispatches on this base and hands the host to transfer, which performs the copy over the host's own transport. The base lives here (not beside a concrete descriptor) so the driver can dispatch on it without importing a namespace module, which would be circular: modules.files.files imports core, and core and the drivers sit above commands.

WaitCommand dataclass

WaitCommand()

Base for descriptors that poll the host until a condition holds.

The second kind of non-shell descriptor, beside TransferCommand: a wait has no shell() rendering -- nothing is sent as shell text. send_command dispatches on this base and hands the host to wait, which polls (re-reading a fact directly, never through the per-step FactCache, which would pin the first value) and sleeps between attempts until the condition holds or the timeout expires, raising WaitTimeoutError in the latter case. Like a transfer it is unprivileged: the sudo/su flags do not apply to a poll.

Waiting lives in a descriptor rather than in the operation body because the prepare phase never sends: an operation that polled inside its own generator would spin forever against the prepare cache. The operation only describes the wait; the driver performs it at send time. The base lives here (not beside a concrete descriptor) for the same reason as TransferCommand: the driver must dispatch on it without importing a namespace module.

WaitTimeoutError

Raised by a WaitCommand whose condition was not met within its timeout.

Propagates out of send_command and therefore out of run_apply -- a wait that never completes aborts the run, exactly as a failing shell command does via CommandError.