Skip to content

Commit 2aff5ac

Browse files
committed
Docs(fix[automation]): Stop check_for_errors matching the echo
why: The rest of this page now waits on tmux channels and matches whole output lines, but check_for_errors was left scanning the joined pane for a substring. That is the same echo race the page warns about, in the one helper where it is easiest to walk into: send_keys types the command, the shell echoes it back, and `mytool --log-level=ERROR` is reported as a crash before the tool has printed anything. The tag also has to survive wrapping. A long command wraps, so a tag that merely sits mid-line in what you typed can still end up starting a continuation line. Letting printf assemble the tag from a format and an argument keeps the literal out of the echo entirely. what: - Scan only the lines the command tags as its own, via command_output() - Take those lines, not a pane, so the helper cannot be handed the echo - Show it both ways: a healthy command whose own arguments say ERROR is not flagged, and a command that really fails is - Record the fix in CHANGES; the racy recipes shipped in 0.61.0
1 parent c3cedfb commit 2aff5ac

2 files changed

Lines changed: 61 additions & 16 deletions

File tree

CHANGES

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,19 @@ _Notes on the upcoming release will go here._
4747

4848
### Documentation
4949

50+
#### The automation recipes no longer race the shell's echo (#709)
51+
52+
Every wait helper in {ref}`automation-patterns` returned the moment the command was
53+
*sent*, not when it finished. {meth}`~libtmux.Pane.send_keys` types into the pane, so
54+
the shell echoes it straight back, and the helpers matched their completion marker
55+
against that echo. A copied `wait_for_output(pane, 'READY')` returned `True` in
56+
milliseconds, before the command had printed anything, and its timeout branch was
57+
unreachable. `check_for_errors` had the same fault in reverse: it reported a crash for
58+
any healthy command whose own arguments happened to mention an error.
59+
60+
The recipes now synchronize through {meth}`~libtmux.Server.wait_for` channels where the
61+
command is yours to change, and where a poll is genuinely needed they match complete
62+
output lines that cannot also occur in the echoed input.
5063
#### Cleaner `from_env` examples (#719)
5164

5265
The rendered examples for {meth}`Pane.from_env() <libtmux.Pane.from_env>` and

docs/topics/automation_patterns.md

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -149,35 +149,67 @@ True
149149

150150
### Detecting errors in output
151151

152-
Waiting for success is only half the job — you also want to notice failure. The same
153-
capture-and-scan approach works for spotting error patterns, so you can bail out
154-
early instead of timing out on a command that already crashed.
152+
Waiting for success is only half the job — you also want to notice failure, so you
153+
can bail out early instead of timing out on a command that already crashed.
154+
155+
Scan the command's **output**, never the whole pane. The shell echoes what you sent
156+
into the same buffer, so a pane-wide substring search reports a crash for
157+
`mytool --log-level=ERROR` before the tool has printed anything at all — it is
158+
matching your own command line. That is the same trap the polls above avoid, in the
159+
one place it is easiest to walk into.
160+
161+
So have the command tag the lines it owns, and scan only those. The tag has to be one
162+
that cannot begin a line of the echoed command — and a long command *wraps*, so "it
163+
is not at the start of my command" is not good enough. Let the shell assemble it:
164+
below, `printf` builds `out:` from a format and a separate argument, so the literal
165+
never appears in what you typed, wrapped or not.
155166

156167
```python
157168
>>> error_window = session.new_window(window_name='error-check', attach=False)
158169
>>> error_pane = error_window.active_pane
159170

160-
>>> def check_for_errors(pane, patterns=None):
161-
... """Check pane output for error patterns."""
171+
>>> def command_output(pane, tag='out:'):
172+
... """The lines the command tagged as its own."""
173+
... return [
174+
... line.removeprefix(tag)
175+
... for line in pane.capture_pane()
176+
... if line.startswith(tag)
177+
... ]
178+
179+
>>> def check_for_errors(lines, patterns=None):
180+
... """Return the first error pattern found in *lines*, or None."""
162181
... if patterns is None:
163182
... patterns = ['Error:', 'error:', 'ERROR', 'FAILED', 'Exception']
164-
... output = '\\n'.join(pane.capture_pane())
165-
... for pattern in patterns:
166-
... if pattern in output:
167-
... return pattern
183+
... for line in lines:
184+
... for pattern in patterns:
185+
... if pattern in line:
186+
... return pattern
168187
... return None
169188

170-
>>> error_channel = 'libtmux-error-check'
189+
A command that succeeds, even though its own command line says `ERROR`:
190+
191+
>>> ch = 'libtmux-errors'
171192
>>> error_pane.send_keys(
172-
... "printf 'Success%s\\n' '!'; "
173-
... f"tmux wait-for -S {error_channel}"
193+
... "true --log-level=ERROR; "
194+
... "printf 'out%s%s\\n' : ok; "
195+
... f"tmux wait-for -S {ch}"
174196
... )
175-
>>> server.wait_for(error_channel)
176-
>>> 'Success!' in error_pane.capture_pane()
177-
True
178-
>>> check_for_errors(error_pane) is None
197+
>>> server.wait_for(ch)
198+
>>> command_output(error_pane)
199+
['ok']
200+
>>> check_for_errors(command_output(error_pane)) is None
179201
True
180202

203+
And one that genuinely fails:
204+
205+
>>> error_pane.send_keys(
206+
... "printf 'out%sError: no disk\\n' :; "
207+
... f"tmux wait-for -S {ch}"
208+
... )
209+
>>> server.wait_for(ch)
210+
>>> check_for_errors(command_output(error_pane))
211+
'Error:'
212+
181213
>>> # Clean up
182214
>>> error_window.kill()
183215
```

0 commit comments

Comments
 (0)