Skip to content

munet: add Commander.async_spawn() - #58

Merged
choppsv1 merged 2 commits into
LabNConsulting:mainfrom
liambrady:liambrady/qemu_pexpect_nonblocking
Sep 29, 2025
Merged

choppsv1 merged 2 commits into
LabNConsulting:mainfrom
liambrady:liambrady/qemu_pexpect_nonblocking

Conversation

@liambrady

@liambrady liambrady commented May 21, 2025 •

Copy link
Copy Markdown
Contributor

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. Some async methods call
spawn(), however, which leads to such methods
being blocked.

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 QEMU nodes immediately and that
independent progress can be made in each VM's
expect/send loop simultaneously.

@codecov

codecov Bot commented May 21, 2025 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.85507% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.12%. Comparing base (b310137) to head (ed8bb5d).
⚠️ Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
munet/base.py 89.70% 7 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #58      +/-   ##
==========================================
+ Coverage   59.66%   60.12%   +0.46%     
==========================================
  Files          19       19              
  Lines        5781     5831      +50     
==========================================
+ Hits         3449     3506      +57     
+ Misses       2332     2325       -7     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@liambrady
liambrady requested a review from choppsv1 May 21, 2025 16:53
@liambrady
liambrady marked this pull request as ready for review May 21, 2025 16:53
@choppsv1

Copy link
Copy Markdown
Collaborator

As discussed elsewhere, we need to use a new async_spawn() variant for this to preserve the existing non-asyncio API. The use case you're interested in is coming from an async function so should be fine to just switch to the new variant inside console/shell_spawn.

Also, since async/await only fails with non-pty pexpect.popen_spawn (i.e., pipes) you should restrict the busy loop fix to only that case. For use_pty == True use the supported async from pexpect.

@liambrady
liambrady force-pushed the liambrady/qemu_pexpect_nonblocking branch from e3b5d2c to a848814 Compare May 26, 2025 21:10
@liambrady liambrady changed the title munet: make Commander.spawn() async munet: add Commander.asynnc_spawn() May 26, 2025
@liambrady liambrady changed the title munet: add Commander.asynnc_spawn() munet: add Commander.async_spawn() May 26, 2025
@liambrady

liambrady commented May 26, 2025 •

Copy link
Copy Markdown
Contributor Author

As discussed elsewhere, we need to use a new async_spawn() variant for this to preserve the existing non-asyncio API. The use case you're interested in is coming from an async function so should be fine to just switch to the new variant inside console/shell_spawn.

I modified both console/shell_spawn and launch/monitor to use the new async variant (both methods are already async and so should use the variant), and now spawn() appears to have fallen out of the code coverage. It appears that commander.spawn() is not called anywhere else within the codebase.

Does this affect the decision to continue to maintain commander.spawn()? Do we need it for future additions to the munet API? It doesn't feel correct to leave dead code within the project (or write new tests to maintain a section of code that isn't being meaningfully used.)

Comment thread munet/base.py

@choppsv1 choppsv1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to test both async and non-async variants, and the vty and non-vty case.

Comment thread munet/base.py
Comment thread munet/base.py
Comment thread munet/base.py
Comment thread munet/base.py
@liambrady
liambrady requested a review from choppsv1 June 25, 2025 18:02
@liambrady
liambrady force-pushed the liambrady/qemu_pexpect_nonblocking branch from fc1a560 to 1b42c00 Compare August 12, 2025 16:37
@liambrady

Copy link
Copy Markdown
Contributor Author

linted new tests

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. Some async methods call
spawn(), however, which leads to such methods
being blocked.

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 QEMU nodes immediately and that
independent progress can be made in each VM's
expect/send loop simultaneously.

Signed-off-by: Liam Brady <lbrady@labn.net>
tests: cover error cases, both sync/async

This commit introduces a minor refactor that collects
sync code used by both sync/async spawn() into a
single callable method. The majority of sync/async
spawn() is untouched due to the fundamental difference
that asyncio brings with it to the send/expect loop.

New tests are introduced such that both sync/async
spawn() methods are tested separately. Movever,
the error cases are also now tested.

Signed-off-by: Liam Brady <lbrady@labn.net>
@choppsv1
choppsv1 force-pushed the liambrady/qemu_pexpect_nonblocking branch from 1b42c00 to ed8bb5d Compare September 29, 2025 19:39
@choppsv1
choppsv1 merged commit c7da29d into LabNConsulting:main Sep 29, 2025
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants