Failures, observation deadlines, and stopping¶
Distinguish process failure from observation timeout and actual termination. These examples run on all three backends without relying on arbitrary sleeps.
- Inspect a normal exit code of 7 with
check=False(the default), then repeat withcheck=Trueand catchCommandFailedError. The exception carries the already-finalized result. PowerShell native scripts also exposelast_success,native_exit_code, andterminating_errorinresult.status. - Start a program waiting for EOF. A zero-second
wait_timeoutexpires only this observation; close stdin afterward and the same command succeeds.ping()executes a health check;snapshot()and repr only read cached state. - Stop another waiting program explicitly with
handle.stop(). Inspect cleanup andsession_reusable, then close the session instead of assuming reuse.
timeout=10 bounds command execution, including input waiting. For independent
budgets use CommandOptions(timeouts=TimeoutPolicy(execution=30, acquire=5,
drain=2)). Timeout initiates cleanup; it is not an exact return-time guarantee.
With check=True, an execution deadline raises CommandTimeoutError, and an
explicit stop observed through a checked wait raises CommandStoppedError.
The mocking chapter demonstrates an execution timeout without real delays.
Cancelling handle.wait() cancels observation only. Cancelling shell.run()
or letting cancellation/another exception escape a command scope requests
termination because that scope owns the work. Forceful cleanup breaks the
session; create a new one explicitly. The outer context closes all owned work;
aclose() also supports explicit, idempotent closure when a context is unsuitable.
Full example¶
File: examples/07-errors-and-lifecycle.py.
python examples/07-errors-and-lifecycle.py bash
On Windows replace bash with powershell or cmd.
"""Inspect failures, time out an observation, and explicitly stop an owned command."""
import asyncio
import os
import sys
from shell_next import (
Backend,
CommandOptions,
Outcome,
ProcessCommand,
SessionConfig,
StdinMode,
use_shell_session,
)
from shell_next.errors import CommandFailedError
async def main(backend: Backend) -> None:
config = SessionConfig(backend=backend, startup_timeout=30)
async with use_shell_session(config) as shell:
# 1. A nonzero exit is a normal EXITED outcome with success=False.
failure = ProcessCommand(sys.executable, ("-c", "import sys; sys.exit(7)"))
result = await shell.run(failure)
assert result.outcome == Outcome.EXITED and not result.success
assert result.status.code == 7
try:
await shell.run(failure, check=True)
except CommandFailedError as error:
assert error.result.command is failure
assert error.result.status.code == 7
else:
raise AssertionError("check=True must report failure")
# 2. This process waits for EOF; timing out observation does not stop it.
command = ProcessCommand(
sys.executable,
("-u", "-c", "import sys; print('ready', flush=True); sys.stdin.read(); print('done')"),
)
options = CommandOptions(stdin=StdinMode.MANUAL)
async with shell.command(command, options=options, timeout=10) as handle:
await handle.expect(b"ready", timeout=5)
try:
await handle.wait(wait_timeout=0)
except TimeoutError:
pass
else:
raise AssertionError("The process is still waiting for EOF")
await handle.close_stdin()
assert (await handle.wait()).success
await shell.ping()
assert shell.is_usable
# 3. A deliberate stop finalizes cleanup; do not assume session reuse.
async with shell.command(command, options=options, timeout=10) as handle:
await handle.expect(b"ready", timeout=5)
result = await handle.stop()
assert result.outcome == Outcome.STOPPED
print("Reusable:", result.session_reusable, "Cleanup:", result.cleanup)
# No more commands are submitted; the outer scope closes this session.
if __name__ == "__main__":
selected = sys.argv[1] if len(sys.argv) > 1 else ("cmd" if os.name == "nt" else "bash")
asyncio.run(main(Backend(selected)))