Skip to content

Commit e3b5d2c

Browse files
committed
munet: make Commander.spawn() async
spawn(), which conducts a send/expect process is blocking due to the abscence of async_=True to pexpect.expect() within the method's main send/expect loop. This is undesirable due the presence of other ongoing initialization coroutines, since it will block their progress for the entire time needed to complete either the single nodes send/expect loop (or wait for it to timeout). This is particularly problematic in cases where multiple VMs requiring custom console expect/send prompts are required (e.g. routers). Since each QEMU VM sets up both console logging and completes an expect/send loop without yielding control to the coroutine of the other QEMU VMs, only one VM can ever have logging configured (and thus advance in an expect/send loop) at any given time. Not only is this inefficient given that all QEMU VMs are running and waiting for input, but this can be fatal if console logging is not set up on each VM in time. If some mandatory console output is missed, then there is no way to recover and the setup of the QEMU VM will time out. However, setting async_=True in pexpect.expect() leads to errors when mixed with PopenSpawn. To bypass this issue, we do not use pexpect.expect() with async_=True, but repeatedly call pexpect.expect() with a timeout set at 0.1 (Until a timeout that we track expires). The end result of this change is that both logging is set up on all nodes immediately in QEMU nodes and that independent progress can be made in each VM's expect/send loop. Signed-off-by: Liam Brady <[email protected]>
1 parent 2c87e57 commit e3b5d2c

File tree

2 files changed

+40
-31
lines changed

2 files changed

+40
-31
lines changed

munet/base.py

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -603,7 +603,7 @@ def _spawn(self, cmd, skip_pre_cmd=False, use_pty=False, echo=False, **kwargs):
603603
p = pexpect.spawn(actual_cmd[0], actual_cmd[1:], echo=echo, **defaults)
604604
return p, actual_cmd
605605

606-
def spawn(
606+
async def spawn(
607607
self,
608608
cmd,
609609
spawned_re,
@@ -616,7 +616,7 @@ def spawn(
616616
trace=None,
617617
**kwargs,
618618
):
619-
"""Create a spawned send/expect process.
619+
"""Create an async spawned send/expect process.
620620
621621
Args:
622622
cmd: list of args to exec/popen with, or an already open socket
@@ -677,12 +677,18 @@ def spawn(
677677

678678
# Now send a CRLF to cause the prompt (or whatever else) to re-issue
679679
p.send("\n")
680-
try:
681-
patterns = [spawned_re, *expects]
680+
patterns = [spawned_re, *expects]
682681

683-
self.logger.debug("%s: expecting: %s", self, patterns)
682+
self.logger.debug("%s: expecting: %s", self, patterns)
684683

685-
while index := p.expect(patterns):
684+
timeout = kwargs.get("timeout", 120)
685+
timeout_ts = datetime.datetime.now() + datetime.timedelta(seconds=timeout)
686+
while timeout_ts > datetime.datetime.now():
687+
try:
688+
await asyncio.sleep(0) # Avoid blocking other coroutines
689+
index = p.expect(patterns, timeout=0.1)
690+
if index == 0:
691+
break
686692
if trace:
687693
assert p.match is not None
688694
self.logger.debug(
@@ -697,35 +703,38 @@ def spawn(
697703
p.send(sends[index - 1])
698704

699705
self.logger.debug("%s: expecting again: %s", self, patterns)
700-
self.logger.debug(
701-
"%s: got spawned_re: '%s' matching '%s'",
702-
self,
703-
p.match.group(0),
704-
spawned_re,
705-
)
706-
return p
707-
except pexpect.TIMEOUT:
706+
except pexpect.TIMEOUT:
707+
continue
708+
except pexpect.EOF as eoferr:
709+
if p.isalive():
710+
raise
711+
rc = p.status
712+
before = indent(p.before)
713+
error = CalledProcessError(rc, ac, output=before)
714+
self.logger.error(
715+
"%s: EOF looking for spawned_re '%s' before EOF:\n%s",
716+
self,
717+
spawned_re,
718+
before,
719+
)
720+
p.close()
721+
raise error from eoferr
722+
723+
if p.match is None:
708724
self.logger.error(
709725
"%s: TIMEOUT looking for spawned_re '%s' expect buffer so far:\n%s",
710726
self,
711727
spawned_re,
712728
indent(p.buffer),
713729
)
714-
raise
715-
except pexpect.EOF as eoferr:
716-
if p.isalive():
717-
raise
718-
rc = p.status
719-
before = indent(p.before)
720-
error = CalledProcessError(rc, ac, output=before)
721-
self.logger.error(
722-
"%s: EOF looking for spawned_re '%s' before EOF:\n%s",
723-
self,
724-
spawned_re,
725-
before,
726-
)
727-
p.close()
728-
raise error from eoferr
730+
raise pexpect.TIMEOUT("spawn() timed out within Munet")
731+
self.logger.debug(
732+
"%s: got spawned_re: '%s' matching '%s'",
733+
self,
734+
p.match.group(0),
735+
spawned_re,
736+
)
737+
return p
729738

730739
async def shell_spawn(
731740
self,
@@ -761,7 +770,7 @@ async def shell_spawn(
761770
combined_prompt = r"({}|{})".format(re.escape(PEXPECT_PROMPT), prompt)
762771

763772
assert not is_file_like(cmd) or not use_pty
764-
p = self.spawn(
773+
p = await self.spawn(
765774
cmd,
766775
combined_prompt,
767776
expects=expects,

munet/native.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -911,7 +911,7 @@ async def monitor(
911911
logfile_read = open(lfname, "a+", encoding="utf-8")
912912
logfile_read.write("-- start read logging for: '{}' --\n".format(sock))
913913

914-
p = self.spawn(sock, prompt, logfile=logfile, logfile_read=logfile_read)
914+
p = await self.spawn(sock, prompt, logfile=logfile, logfile_read=logfile_read)
915915
from .base import ShellWrapper # pylint: disable=C0415
916916

917917
p.send("\n")

0 commit comments

Comments
 (0)