Remove service name from aspects #12; minor tweaks

- Remove service name from RNS destination aspects. Service name
  now selects a suffix for the identity file and should only be
  supplied on the listener. The initiator only needs the destination
  hash of the listener to connect.
- Show a spinner during link establishment on tty sessions
- Attempt to catch and beautify exceptions on initiator
This commit is contained in:
Aaron Heise 2023-02-22 19:58:03 -06:00
parent bd12efd7cf
commit a07ce53bf9
No known key found for this signature in database
GPG Key ID: 6BA54088C41DE8BF
7 changed files with 199 additions and 110 deletions

View File

@ -26,6 +26,14 @@ There will sometimes be breaking changes in the protocol between
releases. Use at your own peril! releases. Use at your own peril!
## Recent Changes ## Recent Changes
### v0.0.12
- Remove service name from RNS destination aspects. Service name
now selects a suffix for the identity file and should only be
supplied on the listener. The initiator only needs the destination
hash of the listener to connect.
- Show a spinner during link establishment on tty sessions
- Attempt to catch and beautify exceptions on initiator
### v0.0.11 ### v0.0.11
- Event loop bursting improves throughput and CPU utilization on - Event loop bursting improves throughput and CPU utilization on
both listener and initiator. both listener and initiator.
@ -40,41 +48,6 @@ releases. Use at your own peril!
- Switch to a new packet-based protocol - Switch to a new packet-based protocol
- Bug fixes and dependency updates - Bug fixes and dependency updates
### v0.0.8
- Improved test suite exposed several issues with the handling of
command line arguments which are now fixed
- Fixed a race condition that would cause remote characters to be
lost intermittently when running remote commands that finish
immediately.
- Added automated testing that actually spins up a random listener
and initiator in a private Reticulum network and passes data
between them, uncovering more issues which are now fixed.
- Fixed (hopefully) an issue where `rnsh` doesn't know what
version it is.
### v0.0.7
Added `-A` command line option. This listener option causes the
remote command line to be appended to the arguments list of the
launched program. This allows the listener to jail connections
to a particular executable while still allowing parameters.
### v0.0.6
Minor improvements in transport efficiency
### v0.0.5
#### Remote command line and pipe compatibility
Command line options have changed somewhat to allow the initiator
to supply a command line. This allows `rnsh` to function similarly
to SSH. You can pipe into or out of `rnsh` to send input through
remote commands or remote command output through other commands.
This behavior can be blocked on the listener with the `-C` option.
When the initiator does not supply a command, the listener uses
a default command specified on its command line. If a default
command is not specified, the listener falls back to the shell
of the user it is running under.
## Quickstart ## Quickstart
Tested (thus far) on Python 3.11 macOS 13.1 ARM64. Should Tested (thus far) on Python 3.11 macOS 13.1 ARM64. Should
@ -98,9 +71,8 @@ rnsh -l -p
# On initiator # On initiator
rnsh -p rnsh -p
``` ```
Note: if you are using a non-default identity or service name, be Note: service name no longer is supplied on initiator. The destination
sure to supply these options with `-p` as the identity and hash encapsulates this information.
destination hashes will change depending on these settings.
#### Listener #### Listener
- Listening for default service name ("default"). - Listening for default service name ("default").
@ -123,20 +95,20 @@ rnsh a5f72aefc2cb3cdba648f73f77c4e887
## Options ## Options
``` ```
Usage: Usage:
rnsh [--config <configdir>] [-i <identityfile>] [-s <service_name>] [-l] -p rnsh -l [-c <configdir>] [-i <identityfile> | -s <service_name>] [-v... | -q...] -p
rnsh -l [--config <configfile>] [-i <identityfile>] [-s <service_name>] rnsh -l [-c <configdir>] [-i <identityfile> | -s <service_name>] [-v... | -q...]
[-v... | -q...] [-b <period>] (-n | -a <identity_hash> [-a <identity_hash>] ...) [-b <period>] (-n | -a <identity_hash> [-a <identity_hash>] ...) [-A | -C]
[-A | -C] [[--] <program> [<arg> ...]] [[--] <program> [<arg> ...]]
rnsh [--config <configfile>] [-i <identityfile>] [-s <service_name>] rnsh [-c <configdir>] [-i <identityfile>] [-v... | -q...] -p
[-v... | -q...] [-N] [-m] [-w <timeout>] <destination_hash> rnsh [-c <configdir>] [-i <identityfile>] [-v... | -q...] [-N] [-m] [-w <timeout>]
[[--] <program> [<arg> ...]] <destination_hash> [[--] <program> [<arg> ...]]
rnsh -h rnsh -h
rnsh --version rnsh --version
Options: Options:
--config DIR Alternate Reticulum config directory to use -c DIR --config DIR Alternate Reticulum config directory to use
-i FILE --identity FILE Specific identity file to use -i FILE --identity FILE Specific identity file to use
-s NAME --service NAME Listen on/connect to specific service name if not default -s NAME --service NAME Service name for identity file if not default
-p --print-identity Print identity information and exit -p --print-identity Print identity information and exit
-l --listen Listen (server) mode. If supplied, <program> <arg>...will -l --listen Listen (server) mode. If supplied, <program> <arg>...will
be used as the command line when the initiator does not be used as the command line when the initiator does not
@ -171,14 +143,30 @@ with an RNS identity, and a service name. Together, RNS makes
these into a destination hash that can be used to connect to these into a destination hash that can be used to connect to
your listener. your listener.
Multiple listeners can use the same identity. As long as Each listener must use a unique identity. The `-s` parameter
they are given different service names. They will have can be used to specify a service name, which creates a unique
different destination hashes and not conflict. identity file.
Listeners must be configured with a command line to run (at Listeners can be configured with a command line to run on
least at this time). The identity hash string is set in the connect. Initiators can supply a command line as well. There
environment variable RNS_REMOTE_IDENTITY for use in child are several different options for the way the command line
programs. is handled:
- `-C` no initiator command line is allowed; the connection will
be terminated if one is supplied.
- `-A` initiator-supplied command line is appended to listener-
configured command line
- With neither of these options, the listener will use the first
valid command line from this list:
1. Initiator-supplied command line
2. Listener command line argument
3. Default shell of user listener is running under
If the `-n` option is not set on the listener, the initiator
is required to identify before starting a command. The program
will be started with the initiator's identity hash string is set
in the environment variable `RNS_REMOTE_IDENTITY`.
Listeners are set up using the `-l` flag. Listeners are set up using the `-l` flag.

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "rnsh" name = "rnsh"
version = "0.0.11" version = "0.0.12"
description = "Shell over Reticulum" description = "Shell over Reticulum"
authors = ["acehoss <acehoss@acehoss.net>"] authors = ["acehoss <acehoss@acehoss.net>"]
license = "MIT" license = "MIT"

View File

@ -18,20 +18,20 @@ def _split_array_at(arr: [_T], at: _T) -> ([_T], [_T]):
usage = \ usage = \
''' '''
Usage: Usage:
rnsh [--config <configdir>] [-i <identityfile>] [-s <service_name>] [-l] -p rnsh -l [-c <configdir>] [-i <identityfile> | -s <service_name>] [-v... | -q...] -p
rnsh -l [--config <configfile>] [-i <identityfile>] [-s <service_name>] rnsh -l [-c <configdir>] [-i <identityfile> | -s <service_name>] [-v... | -q...]
[-v... | -q...] [-b <period>] (-n | -a <identity_hash> [-a <identity_hash>] ...) [-b <period>] (-n | -a <identity_hash> [-a <identity_hash>] ...) [-A | -C]
[-A | -C] [[--] <program> [<arg> ...]] [[--] <program> [<arg> ...]]
rnsh [--config <configfile>] [-i <identityfile>] [-s <service_name>] rnsh [-c <configdir>] [-i <identityfile>] [-v... | -q...] -p
[-v... | -q...] [-N] [-m] [-w <timeout>] <destination_hash> rnsh [-c <configdir>] [-i <identityfile>] [-v... | -q...] [-N] [-m] [-w <timeout>]
[[--] <program> [<arg> ...]] <destination_hash> [[--] <program> [<arg> ...]]
rnsh -h rnsh -h
rnsh --version rnsh --version
Options: Options:
--config DIR Alternate Reticulum config directory to use -c DIR --config DIR Alternate Reticulum config directory to use
-i FILE --identity FILE Specific identity file to use -i FILE --identity FILE Specific identity file to use
-s NAME --service NAME Listen on/connect to specific service name if not default -s NAME --service NAME Service name for identity file if not default
-p --print-identity Print identity information and exit -p --print-identity Print identity information and exit
-l --listen Listen (server) mode. If supplied, <program> <arg>...will -l --listen Listen (server) mode. If supplied, <program> <arg>...will
be used as the command line when the initiator does not be used as the command line when the initiator does not
@ -59,6 +59,7 @@ Options:
-h --help Show this help -h --help Show this help
''' '''
DEFAULT_SERVICE_NAME = "default"
class Args: class Args:
def __init__(self, argv: [str]): def __init__(self, argv: [str]):
@ -75,9 +76,11 @@ class Args:
args = docopt.docopt(usage, argv=self.docopts_argv[1:], version=f"rnsh {rnsh.__version__}") args = docopt.docopt(usage, argv=self.docopts_argv[1:], version=f"rnsh {rnsh.__version__}")
# json.dump(args, sys.stdout) # json.dump(args, sys.stdout)
self.service_name = args.get("--service", None) or "default"
self.listen = args.get("--listen", None) or False self.listen = args.get("--listen", None) or False
self.service_name = args.get("--service", None)
if self.listen and (self.service_name is None or len(self.service_name) > 0):
self.service_name = DEFAULT_SERVICE_NAME
self.identity = args.get("--identity", None) self.identity = args.get("--identity", None)
self.config = args.get("--config", None) self.config = args.get("--config", None)
self.print_identity = args.get("--print-identity", None) or False self.print_identity = args.get("--print-identity", None) or False

View File

@ -93,11 +93,17 @@ def _sigint_handler(sig, frame):
signal.signal(signal.SIGINT, _sigint_handler) signal.signal(signal.SIGINT, _sigint_handler)
def _prepare_identity(identity_path): def _sanitize_service_name(service_name:str) -> str:
return re.sub(r'\W+', '', service_name)
def _prepare_identity(identity_path, service_name: str = None):
global _identity global _identity
log = _get_logger("_prepare_identity") log = _get_logger("_prepare_identity")
service_name = _sanitize_service_name(service_name or "")
if identity_path is None: if identity_path is None:
identity_path = RNS.Reticulum.identitypath + "/" + APP_NAME identity_path = RNS.Reticulum.identitypath + "/" + APP_NAME + \
(f".{service_name}" if service_name and len(service_name) > 0 else "")
if os.path.isfile(identity_path): if os.path.isfile(identity_path):
_identity = RNS.Identity.from_file(identity_path) _identity = RNS.Identity.from_file(identity_path)
@ -111,26 +117,32 @@ def _prepare_identity(identity_path):
def _print_identity(configdir, identitypath, service_name, include_destination: bool): def _print_identity(configdir, identitypath, service_name, include_destination: bool):
global _reticulum global _reticulum
_reticulum = RNS.Reticulum(configdir=configdir, loglevel=RNS.LOG_INFO) _reticulum = RNS.Reticulum(configdir=configdir, loglevel=RNS.LOG_INFO)
_prepare_identity(identitypath) if service_name and len(service_name) > 0:
destination = RNS.Destination(_identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, service_name) print(f"Using service name \"{service_name}\"")
_prepare_identity(identitypath, service_name)
destination = RNS.Destination(_identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME)
print("Identity : " + str(_identity)) print("Identity : " + str(_identity))
if include_destination: if include_destination:
print("Listening on : " + RNS.prettyhexrep(destination.hash)) print("Listening on : " + RNS.prettyhexrep(destination.hash))
exit(0) exit(0)
async def _listen(configdir, command, identitypath=None, service_name="default", verbosity=0, quietness=0, allowed=None, async def _listen(configdir, command, identitypath=None, service_name=None, verbosity=0, quietness=0, allowed=None,
disable_auth=None, announce_period=900, no_remote_command=True, remote_cmd_as_args=False): disable_auth=None, announce_period=900, no_remote_command=True, remote_cmd_as_args=False):
global _identity, _allow_all, _allowed_identity_hashes, _reticulum, _cmd, _destination, _no_remote_command global _identity, _allow_all, _allowed_identity_hashes, _reticulum, _cmd, _destination, _no_remote_command
global _remote_cmd_as_args global _remote_cmd_as_args
log = _get_logger("_listen") log = _get_logger("_listen")
if service_name is None or len(service_name) == 0:
service_name = "default"
log.info(f"Using service name {service_name}")
targetloglevel = RNS.LOG_INFO + verbosity - quietness targetloglevel = RNS.LOG_INFO + verbosity - quietness
_reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel) _reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel)
rnslogging.RnsHandler.set_log_level_with_rns_level(targetloglevel) rnslogging.RnsHandler.set_log_level_with_rns_level(targetloglevel)
_prepare_identity(identitypath) _prepare_identity(identitypath, service_name)
_destination = RNS.Destination(_identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, service_name) _destination = RNS.Destination(_identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME)
_cmd = command _cmd = command
if _cmd is None or len(_cmd) == 0: if _cmd is None or len(_cmd) == 0:
@ -218,12 +230,33 @@ async def _listen(configdir, command, identitypath=None, service_name="default",
await asyncio.sleep(0.01) await asyncio.sleep(0.01)
async def _spin(until: callable = None, timeout: float | None = None) -> bool: async def _spin_tty(until=None, msg=None, timeout=None):
i = 0
syms = "⢄⢂⢁⡁⡈⡐⡠"
if timeout != None:
timeout = time.time()+timeout
print(msg+" ", end=" ")
while (timeout == None or time.time()<timeout) and not until():
await asyncio.sleep(0.1)
print(("\b\b"+syms[i]+" "), end="")
sys.stdout.flush()
i = (i+1)%len(syms)
print("\r"+" "*len(msg)+" \r", end="")
if timeout != None and time.time() > timeout:
return False
else:
return True
async def _spin_pipe(until: callable = None, msg=None, timeout: float | None = None) -> bool:
if timeout is not None: if timeout is not None:
timeout += time.time() timeout += time.time()
while (timeout is None or time.time() < timeout) and not until(): while (timeout is None or time.time() < timeout) and not until():
if await _check_finished(0.01): if await _check_finished(0.1):
raise asyncio.CancelledError() raise asyncio.CancelledError()
if timeout is not None and time.time() > timeout: if timeout is not None and time.time() > timeout:
return False return False
@ -231,6 +264,13 @@ async def _spin(until: callable = None, timeout: float | None = None) -> bool:
return True return True
async def _spin(until: callable = None, msg=None, timeout: float | None = None) -> bool:
if os.isatty(1):
return await _spin_tty(until, msg, timeout)
else:
return await _spin_pipe(until, msg, timeout)
_link: RNS.Link | None = None _link: RNS.Link | None = None
_remote_exec_grace = 2.0 _remote_exec_grace = 2.0
_new_data: asyncio.Event | None = None _new_data: asyncio.Event | None = None
@ -266,7 +306,7 @@ class RemoteExecutionError(Exception):
async def _initiate_link(configdir, identitypath=None, verbosity=0, quietness=0, noid=False, destination=None, async def _initiate_link(configdir, identitypath=None, verbosity=0, quietness=0, noid=False, destination=None,
service_name="default", timeout=RNS.Transport.PATH_REQUEST_TIMEOUT): timeout=RNS.Transport.PATH_REQUEST_TIMEOUT):
global _identity, _reticulum, _link, _destination, _remote_exec_grace, _tr, _new_data global _identity, _reticulum, _link, _destination, _remote_exec_grace, _tr, _new_data
log = _get_logger("_initiate_link") log = _get_logger("_initiate_link")
@ -291,7 +331,8 @@ async def _initiate_link(configdir, identitypath=None, verbosity=0, quietness=0,
if not RNS.Transport.has_path(destination_hash): if not RNS.Transport.has_path(destination_hash):
RNS.Transport.request_path(destination_hash) RNS.Transport.request_path(destination_hash)
log.info(f"Requesting path...") log.info(f"Requesting path...")
if not await _spin(until=lambda: RNS.Transport.has_path(destination_hash), timeout=timeout): if not await _spin(until=lambda: RNS.Transport.has_path(destination_hash), msg="Requesting path...",
timeout=timeout):
raise RemoteExecutionError("Path not found") raise RemoteExecutionError("Path not found")
if _destination is None: if _destination is None:
@ -300,8 +341,7 @@ async def _initiate_link(configdir, identitypath=None, verbosity=0, quietness=0,
listener_identity, listener_identity,
RNS.Destination.OUT, RNS.Destination.OUT,
RNS.Destination.SINGLE, RNS.Destination.SINGLE,
APP_NAME, APP_NAME
service_name
) )
if _link is None or _link.status == RNS.Link.PENDING: if _link is None or _link.status == RNS.Link.PENDING:
@ -312,7 +352,8 @@ async def _initiate_link(configdir, identitypath=None, verbosity=0, quietness=0,
_link.set_link_closed_callback(_client_link_closed) _link.set_link_closed_callback(_client_link_closed)
log.info(f"Establishing link...") log.info(f"Establishing link...")
if not await _spin(until=lambda: _link.status == RNS.Link.ACTIVE, timeout=timeout): if not await _spin(until=lambda: _link.status == RNS.Link.ACTIVE, msg="Establishing link...",
timeout=timeout):
raise RemoteExecutionError("Could not establish link with " + RNS.prettyhexrep(destination_hash)) raise RemoteExecutionError("Could not establish link with " + RNS.prettyhexrep(destination_hash))
log.debug("Have link") log.debug("Have link")
@ -323,8 +364,16 @@ async def _initiate_link(configdir, identitypath=None, verbosity=0, quietness=0,
_link.set_packet_callback(_client_packet_handler) _link.set_packet_callback(_client_packet_handler)
async def _handle_error(errmsg: protocol.Message):
if isinstance(errmsg, protocol.ErrorMessage):
with contextlib.suppress(Exception):
if _link and _link.status == RNS.Link.ACTIVE:
_link.teardown()
await asyncio.sleep(0.1)
raise RemoteExecutionError(f"Remote error: {errmsg.msg}")
async def _initiate(configdir: str, identitypath: str, verbosity: int, quietness: int, noid: bool, destination: str, async def _initiate(configdir: str, identitypath: str, verbosity: int, quietness: int, noid: bool, destination: str,
service_name: str, timeout: float, command: [str] | None = None): timeout: float, command: [str] | None = None):
global _new_data, _finished, _tr, _cmd, _pre_input global _new_data, _finished, _tr, _cmd, _pre_input
log = _get_logger("_initiate") log = _get_logger("_initiate")
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
@ -339,7 +388,6 @@ async def _initiate(configdir: str, identitypath: str, verbosity: int, quietness
quietness=quietness, quietness=quietness,
noid=noid, noid=noid,
destination=destination, destination=destination,
service_name=service_name,
timeout=timeout, timeout=timeout,
) )
@ -360,6 +408,7 @@ async def _initiate(configdir: str, identitypath: str, verbosity: int, quietness
try: try:
vp = _pq.get(timeout=max(outlet.rtt * 20, 5)) vp = _pq.get(timeout=max(outlet.rtt * 20, 5))
vm = messenger.receive(vp) vm = messenger.receive(vp)
await _handle_error(vm)
if not isinstance(vm, protocol.VersionInfoMessage): if not isinstance(vm, protocol.VersionInfoMessage):
raise Exception("Invalid message received") raise Exception("Invalid message received")
log.debug(f"Server version info: sw {vm.sw_version} prot {vm.protocol_version}") log.debug(f"Server version info: sw {vm.sw_version} prot {vm.protocol_version}")
@ -433,6 +482,7 @@ async def _initiate(configdir: str, identitypath: str, verbosity: int, quietness
try: try:
packet = _pq.get(timeout=sleeper.next_sleep_time() if not processed else 0.0005) packet = _pq.get(timeout=sleeper.next_sleep_time() if not processed else 0.0005)
message = messenger.receive(packet) message = messenger.receive(packet)
await _handle_error(message)
processed = True processed = True
if isinstance(message, protocol.StreamDataMessage): if isinstance(message, protocol.StreamDataMessage):
if message.stream_id == protocol.StreamDataMessage.STREAM_ID_STDOUT: if message.stream_id == protocol.StreamDataMessage.STREAM_ID_STDOUT:
@ -534,22 +584,25 @@ async def _rnsh_cli_main():
remote_cmd_as_args=args.remote_cmd_as_args) remote_cmd_as_args=args.remote_cmd_as_args)
return 0 return 0
if args.destination is not None and args.service_name is not None: if args.destination is not None:
return_code = await _initiate( try:
configdir=args.config, return_code = await _initiate(
identitypath=args.identity, configdir=args.config,
verbosity=args.verbose, identitypath=args.identity,
quietness=args.quiet, verbosity=args.verbose,
noid=args.no_id, quietness=args.quiet,
destination=args.destination, noid=args.no_id,
service_name=args.service_name, destination=args.destination,
timeout=args.timeout, timeout=args.timeout,
command=args.command_line command=args.command_line
) )
return return_code if args.mirror else 0 return return_code if args.mirror else 0
except Exception as ex:
print(f"{ex}")
return 255;
else: else:
print("") print("")
print(args.usage) print(rnsh.args.usage)
print("") print("")
return 1 return 1

View File

@ -354,7 +354,7 @@ class ListenerSession:
message = self.messenger.receive(raw) message = self.messenger.receive(raw)
self._handle_message(message) self._handle_message(message)
except Exception as ex: except Exception as ex:
self._protocol_error("unusable packet") self._protocol_error(f"error receiving packet: {ex}")
class RNSOutlet(LSOutletBase): class RNSOutlet(LSOutletBase):

View File

@ -41,10 +41,9 @@ def test_program_initiate_no_args():
def test_program_initiate_dash_args(): def test_program_initiate_dash_args():
docopt_threw = False docopt_threw = False
try: try:
args = rnsh.args.Args(shlex.split("rnsh --config ~/Projects/rnsh/testconfig -s test -vvvvvvv a5f72aefc2cb3cdba648f73f77c4e887 -- -l")) args = rnsh.args.Args(shlex.split("rnsh --config ~/Projects/rnsh/testconfig -vvvvvvv a5f72aefc2cb3cdba648f73f77c4e887 -- -l"))
assert not args.listen assert not args.listen
assert args.config == "~/Projects/rnsh/testconfig" assert args.config == "~/Projects/rnsh/testconfig"
assert args.service_name == "test"
assert args.verbose == 7 assert args.verbose == 7
assert args.destination == "a5f72aefc2cb3cdba648f73f77c4e887" assert args.destination == "a5f72aefc2cb3cdba648f73f77c4e887"
assert args.command_line == ["-l"] assert args.command_line == ["-l"]
@ -53,6 +52,21 @@ def test_program_initiate_dash_args():
assert not docopt_threw assert not docopt_threw
def test_program_listen_dash_args():
docopt_threw = False
try:
args = rnsh.args.Args(shlex.split("rnsh -l --config ~/Projects/rnsh/testconfig -n -C -- /bin/pwd"))
assert args.listen
assert args.config == "~/Projects/rnsh/testconfig"
assert args.destination is None
assert args.no_auth
assert args.no_remote_cmd
assert args.command_line == ["/bin/pwd"]
except docopt.DocoptExit:
docopt_threw = True
assert not docopt_threw
def test_program_listen_config_print(): def test_program_listen_config_print():
docopt_threw = False docopt_threw = False
try: try:

View File

@ -56,16 +56,18 @@ async def test_rnsh_listen_start_stop():
assert not wrapper.process.running assert not wrapper.process.running
async def get_id_and_dest(td: str) -> tuple[str, str]: async def get_listener_id_and_dest(td: str) -> tuple[str, str]:
with tests.helpers.SubprocessReader(name="getid", argv=shlex.split(f"poetry run -- rnsh -l --config \"{td}\" -p")) as wrapper: with tests.helpers.SubprocessReader(name="getid", argv=shlex.split(f"poetry run -- rnsh -l -c \"{td}\" -p")) as wrapper:
wrapper.start() wrapper.start()
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
assert wrapper.process.running assert wrapper.process.running
# wait for process to start up # wait for process to start up
await tests.helpers.wait_for_condition_async(lambda: not wrapper.process.running, 5) await tests.helpers.wait_for_condition_async(lambda: not wrapper.process.running, 5)
assert not wrapper.process.running assert not wrapper.process.running
await asyncio.sleep(2)
# read the output # read the output
text = wrapper.read().decode("utf-8").replace("\r", "").replace("\n", "") text = wrapper.read().decode("utf-8").replace("\r", "").replace("\n", "")
assert text.index("Using service name \"default\"") is not None
assert text.index("Identity") is not None assert text.index("Identity") is not None
match = re.search(r"<([a-f0-9]{32})>[^<]+<([a-f0-9]{32})>", text) match = re.search(r"<([a-f0-9]{32})>[^<]+<([a-f0-9]{32})>", text)
assert match is not None assert match is not None
@ -77,29 +79,58 @@ async def get_id_and_dest(td: str) -> tuple[str, str]:
return ih, dh return ih, dh
async def get_initiator_id(td: str) -> str:
with tests.helpers.SubprocessReader(name="getid", argv=shlex.split(f"poetry run -- rnsh -c \"{td}\" -p")) as wrapper:
wrapper.start()
await asyncio.sleep(0.1)
assert wrapper.process.running
# wait for process to start up
await tests.helpers.wait_for_condition_async(lambda: not wrapper.process.running, 5)
assert not wrapper.process.running
# read the output
text = wrapper.read().decode("utf-8").replace("\r", "").replace("\n", "")
assert text.index("Identity") is not None
match = re.search(r"<([a-f0-9]{32})>", text)
assert match is not None
ih = match.group(1)
assert len(ih) == 32
await asyncio.sleep(0.1)
return ih
@pytest.mark.skip_ci @pytest.mark.skip_ci
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_rnsh_get_id_and_dest() -> [int]: async def test_rnsh_get_listener_id_and_dest() -> [int]:
with tests.helpers.tempdir() as td: with tests.helpers.tempdir() as td:
ih, dh = await get_id_and_dest(td) ih, dh = await get_listener_id_and_dest(td)
assert len(ih) == 32 assert len(ih) == 32
assert len(dh) == 32 assert len(dh) == 32
@pytest.mark.skip_ci
@pytest.mark.asyncio
async def test_rnsh_get_initiator_id() -> [int]:
with tests.helpers.tempdir() as td:
ih = await get_initiator_id(td)
assert len(ih) == 32
async def do_connected_test(listener_args: str, initiator_args: str, test: callable): async def do_connected_test(listener_args: str, initiator_args: str, test: callable):
with tests.helpers.tempdir() as td: with tests.helpers.tempdir() as td:
ih, dh = await get_id_and_dest(td) ih, dh = await get_listener_id_and_dest(td)
iih = await get_initiator_id(td)
assert len(ih) == 32 assert len(ih) == 32
assert len(dh) == 32 assert len(dh) == 32
with tests.helpers.SubprocessReader(name="listener", argv=shlex.split(f"poetry run -- rnsh -l --config \"{td}\" {listener_args}")) as listener, \ assert len(iih) == 32
tests.helpers.SubprocessReader(name="initiator", argv=shlex.split(f"poetry run -- rnsh --config \"{td}\" {dh} {initiator_args}")) as initiator: with tests.helpers.SubprocessReader(name="listener", argv=shlex.split(f"poetry run -- rnsh -l -c \"{td}\" {listener_args}")) as listener, \
tests.helpers.SubprocessReader(name="initiator", argv=shlex.split(f"poetry run -- rnsh -c \"{td}\" {dh} {initiator_args}")) as initiator:
# listener startup # listener startup
listener.start() listener.start()
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
assert listener.process.running assert listener.process.running
# wait for process to start up # wait for process to start up
await asyncio.sleep(3) await asyncio.sleep(5)
# read the output # read the output
text = listener.read().decode("utf-8") text = listener.read().decode("utf-8")
assert text.index(dh) is not None assert text.index(dh) is not None
@ -108,7 +139,7 @@ async def do_connected_test(listener_args: str, initiator_args: str, test: calla
initiator.start() initiator.start()
assert initiator.process.running assert initiator.process.running
await test(td, ih, dh, listener, initiator) await test(td, ih, dh, iih, listener, initiator)
# expect test to shut down initiator # expect test to shut down initiator
assert not initiator.process.running assert not initiator.process.running
@ -127,13 +158,13 @@ async def do_connected_test(listener_args: str, initiator_args: str, test: calla
async def test_rnsh_get_echo_through(): async def test_rnsh_get_echo_through():
cwd = os.getcwd() cwd = os.getcwd()
async def test(td: str, ih: str, dh: str, listener: tests.helpers.SubprocessReader, async def test(td: str, ih: str, dh: str, iih: str, listener: tests.helpers.SubprocessReader,
initiator: tests.helpers.SubprocessReader): initiator: tests.helpers.SubprocessReader):
start_time = time.time() start_time = time.time()
while initiator.return_code is None and time.time() - start_time < 3: while initiator.return_code is None and time.time() - start_time < 3:
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
text = initiator.read().decode("utf-8").replace("\r", "").replace("\n", "") text = initiator.read().decode("utf-8").replace("\r", "").replace("\n", "")
assert text == cwd assert text[len(text)-len(cwd):] == cwd
await do_connected_test("-n -C -- /bin/pwd", "", test) await do_connected_test("-n -C -- /bin/pwd", "", test)