Streaming and Looping Output
Long-running programs can produce output well before they exit. Session.enter() exposes that output as it arrives, so Python can process a log follower, monitor, backup tool, or packet capture incrementally.
Choosing live interaction or a completed result
Completed shell commands return immutable CommandResult snapshots in 4.0 source, matching single-dollar execution. A synchronous $tcpdump ... assignment waits for completion; three-value unpacking does not provide a running stream. The enter()/expect()/send()/live-buffer examples below retain their existing interaction semantics, including the published 3.1.5 API.
For a separate continuous process, prefer the 4.0 managed job API with streaming output, explicit stop and a finite fallback deadline for unattended agents. In 4.0 source, use console.start() for a foreground job in the current shell/sudo/su context; enter() remains the interactive conversation API. Ctrl-C is a stop request, not proof of remote process termination. See the canonical execution and long-program guide for bounded captures, output conditions, cleanup and termination reporting.
4.0: stream in the current console
console.start(command) returns CommandJob while keeping the current Bash shell’s cwd, environment and identity. Ordinary completed commands remain unchanged. A console has one foreground job; other console operations are rejected until it finishes or stops and verifies recovery. Job operations run on the creating thread. Console start defaults to a 60-second total deadline; prefer an explicit finite deadline for unattended agents.
from getpass import getpass
with $.sudo(password=getpass("sudo password: ")):
with $.start("tcpdump -l -n -i INTERFACE", timeout=30) as job:
try:
for chunk in job.iter_stdout():
print(chunk, end="", flush=True)
finally:
result = job.stop()
result = $hostname
Replace INTERFACE and provide native account/capture permission. Use nested su/sudo APIs for identity changes; direct privilege shells and detached/background jobs are unsupported. Text chunks are not guaranteed lines. PTY stdout/stderr can merge. Ctrl-C recovery must verify the original UID/PID; unresolved recovery fails and disables the console. Pipe consoles support completion but cannot safely cancel a running program. A confirmed shell recovery does not establish that detached descendants stopped. See the executable canonical example.
Stream output
from sshscript import Session
session = Session()
try:
with session.enter("ping -c 4 example.net") as process:
for line in process.stdout(10):
print(line, end="")
finally:
session.close(strict=True)
process.stdout(10) returns an iterator. It yields received stdout chunks, normally lines, and resets its ten-second timer whenever new output arrives. If nothing new arrives before the timeout, it raises TimeoutError.
For a continuous command, supply an exit action:
with session.enter(
"tail -F /var/log/syslog",
exit=chr(3),
) as process:
for line in process.stdout(30):
if "ERROR" in line:
print("alert:", line, end="")
exit=chr(3) sends Ctrl-C when the interactive context ends.
Stop on a condition
matches = 0
with session.enter(
"tail -F /var/log/myapp.log",
exit=chr(3),
) as process:
for line in process.stdout(60):
if "completed" in line:
matches += 1
if matches == 10:
break
Leaving the context after break sends the configured exit action. An intentionally endless command should always have an exit action or another well-defined termination mechanism.
Iterator options
process.stdout(timeout=None, silent=False, shift=True)
| Argument | Behavior |
|---|---|
timeout | Seconds to wait after the last output; None waits indefinitely. |
silent | If False, inactivity raises TimeoutError. If True, iteration ends normally. |
shift | If True, yielded chunks leave the live buffer. If False, the buffer is retained. |
Treat unexpected silence as an error:
try:
with session.enter(
"tcpdump -n -i eth0",
exit=chr(3),
) as process:
for line in process.stdout(15):
process_packet(line)
except TimeoutError as exc:
raise RuntimeError(
"tcpdump produced no output for 15 seconds"
) from exc
For an expected quiet interval, use silent=True:
with session.enter(
"./wait-for-work.sh",
exit=chr(3),
) as process:
for line in process.stdout(5, silent=True):
print(line, end="")
Preserve accumulated output
Iteration consumes output by default. Use shift=False when the complete buffer is needed afterward:
with session.enter(
"./progressive-report.sh",
exit=chr(3),
) as process:
for line in process.stdout(20, shift=False):
print(line, end="")
if "DONE" in line:
break
report = str(process.stdout)
Keep the consuming default for unbounded streams so processed output does not accumulate indefinitely.
Remote and privileged streams
with local.connect("ops@example.net") as remote:
with remote.sudo(password=password) as root:
with root.enter(
"journalctl -f -u nginx",
exit=chr(3),
) as process:
for line in process.stdout(30):
if "error" in line.lower():
print(line, end="")
The output-handling code is the same for local, remote, nested, and privileged Sessions.
Optional Dollar syntax
with $.enter("journalctl -f -u nginx", exit=chr(3)):
for line in $.stdout(30):
if "error" in line.lower():
print(line, end="")
Last Updated: 2026-10-09 10:40:03