1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
//! Prove a pane's shell is at a PRIMARY prompt before a launch is typed at it
//! (#8233 review round 2, findings 1 and 8).
//!
//! Why: `managed_launch::deliver` used to send `C-c` and then immediately type
//! the launch line, with nothing between the two confirming the shell had
//! actually returned to a prompt. Two live failures came out of that gap. A pane
//! whose `.zshrc` was still running (`direnv` stalling on `gh auth token` during
//! the launch `cd`) took the keystrokes into a canonical-mode tty that nothing
//! was reading. A pane sitting at a `quote>` continuation prompt swallowed three
//! successive launch commands as literal text inside one open quote, because
//! `C-c` alone is a hope, not evidence.
//!
//! What: [`confirm_prompt`] types a SHORT fixed-shape probe and waits to see the
//! probe's OUTPUT appear in the pane. Output can only appear once the shell
//! parsed and executed the line, so a pane still in its init hooks, or holding
//! an unterminated construct, produces nothing and the probe times out. On a
//! timeout the recovery is an interrupt — never a closing quote, never a bare
//! Enter, either of which would RUN whatever the open construct had accumulated
//! — and the probe is repeated, so the flush is confirmed rather than assumed.
//!
//! [`continuation_kind`] classifies a captured pane tail so the two failures are
//! reported as DISTINCT states: "wedged at a continuation prompt" and "no prompt
//! yet" call for the same recovery here but are different bugs to an operator,
//! and the launch verifier names them apart.
//!
//! The probe is deliberately not a length check on the launch line. The typed
//! line cannot be partially delivered by construction — it names a launch spec
//! file and is bounded by `core::tmux::MAX_PANE_COMMAND_BYTES`, pinned by
//! `pane_line_is_short_for_the_worst_case_launch` — so there is nothing to
//! echo-scrape. This handshake answers the other half: whether anything is
//! READING the tty at all.
//!
//! Test: `crates/trusty-mpm/src/runtime/pane_handshake_tests.rs`.
use Duration;
use crateManagedTmuxDriver;
/// Probe rounds [`confirm_prompt`] runs before giving up.
///
/// Why: round one asks a pane that is probably fine; an interrupt and round two
/// covers the wedged case. A third round has nothing new to try — if an
/// interrupt plus a full probe window did not produce a prompt, the pane is not
/// one a launch should be typed into.
pub const PROMPT_PROBE_ROUNDS: u32 = 2;
/// Capture polls per round. Paired with [`PROMPT_PROBE_INTERVAL`] this is a
/// 1.5 s window per round, 3 s worst case, and it returns the moment the
/// probe's output appears — typically inside 200 ms.
///
/// Why bounded so tightly: [`confirm_prompt`] is reached from the SYNCHRONOUS
/// `RuntimeAdapter::spawn`, so its waiting is a blocking sleep on the caller's
/// thread. The budget only has to cover a shell finishing its init hooks, which
/// is the case this issue's captures show; a runtime that is merely slow to
/// START is covered much later and much more generously by
/// `daemon::managed_routes::launch_verify::verify_launch`.
pub const PROMPT_PROBE_ATTEMPTS: u32 = 15;
/// Delay between [`confirm_prompt`] captures. See [`PROMPT_PROBE_ATTEMPTS`].
pub const PROMPT_PROBE_INTERVAL: Duration = from_millis;
/// Lines of pane tail [`confirm_prompt`] reads. The probe's output is the last
/// thing printed, so a short tail is enough and keeps the capture cheap.
const PROBE_CAPTURE_LINES: usize = 12;
/// Secondary-prompt renderings that mean "the parser is holding an open
/// construct" (#8233, live evidence).
///
/// Why: these are zsh's `PS2` forms — the observed failures were `quote>` (an
/// unterminated single quote, session `tm-apex-companion`) and `cmdand cursh>`
/// (an unterminated `{` group, session `tm-writing-01`). Recognising them is
/// what separates "wedged" from "not started yet" in the operator-facing
/// report; the RECOVERY for both is the same interrupt.
/// What: matched as a suffix of the pane's last non-empty line, so a composed
/// form like `cmdand cursh>` matches on its final token.
/// Test: `continuation_kind_recognises_an_open_quote`,
/// `continuation_kind_recognises_a_composed_zsh_prompt`,
/// `continuation_kind_ignores_an_ordinary_prompt`.
const CONTINUATION_PROMPTS: & = &;
/// What [`confirm_prompt`] concluded about the pane's shell.
///
/// Why: the caller needs three decisions out of one call — proceed, abandon and
/// say the shell was wedged, abandon and say nothing was reading the tty — plus
/// a fourth answer for a driver that cannot see the pane at all, which must
/// change nothing (every hermetic test and the documented tmux-absent fallback
/// live there).
/// What: only [`Self::Continuation`] and [`Self::Unresponsive`] are failures.
/// Test: one test per variant in `pane_handshake_tests.rs`.
pub
/// Classify a captured pane tail as a shell continuation prompt.
///
/// Why: see [`CONTINUATION_PROMPTS`]. Pure and separate from the I/O so the
/// recognition table can be tested against real captured text.
/// What: takes the last NON-EMPTY line, trims it, and returns the
/// [`CONTINUATION_PROMPTS`] entry it ends with. A line ending in a bare `>` is
/// deliberately NOT matched — that is bash's default `PS2`, and it is also how
/// plenty of ordinary prompts end, so matching it would refuse healthy launches.
/// Test: `continuation_kind_recognises_an_open_quote`,
/// `continuation_kind_recognises_a_composed_zsh_prompt`,
/// `continuation_kind_ignores_an_ordinary_prompt`,
/// `continuation_kind_ignores_empty_output`.
pub
/// The text a probe prints when the shell RUNS it, for launch `nonce`.
///
/// Why: the needle must not appear in the pane merely because the probe was
/// typed. `echo` is present in every POSIX shell and prints nothing else.
/// Test: `the_probe_line_does_not_contain_its_own_output`.
/// The literal prefix of every probe line. See [`probe_line`].
const PROBE_PREFIX: &str = "echo tm-rea\"dy\"-";
/// The probe command line typed at the prompt.
///
/// Why: the quotes split the needle so the TYPED text and the PRINTED text
/// differ — `echo tm-rea"dy"-<nonce>` on screen, `tm-ready-<nonce>` in the
/// output. Scraping for the output alone therefore cannot be satisfied by the
/// echo of the keystrokes, which is the whole point: only execution counts.
/// What: about 30 bytes, far under `core::tmux::MAX_PANE_COMMAND_BYTES`, so the
/// probe is never itself a victim of the truncation it guards against.
/// Test: `the_probe_line_does_not_contain_its_own_output`.
/// What a shell would PRINT for `text`, when `text` is a probe line.
///
/// Why: this is the seam a hermetic [`ManagedTmuxDriver`] double answers the
/// handshake through. Without it every fake driver would be a shell that reads
/// nothing, [`confirm_prompt`] would report [`PaneState::Unresponsive`], and the
/// whole hermetic test corpus would start refusing launches for a reason that
/// exists only in the test double. Keeping the recognition beside the
/// construction is what stops the two drifting.
/// What: `Some(<the printed line>)` for a probe line, `None` for anything else —
/// so a double can also tell a probe apart from the launch line it is actually
/// asserting on.
/// Test: `probe_reply_answers_only_a_probe_line`.
pub
/// Read the pane's tail, pane-scoped when a pane id is known.
///
/// What: `None` when the driver cannot capture — the [`PaneState::Unobservable`]
/// signal, never an assertion that the pane is bad.
/// Flush whatever the shell's parser is holding.
///
/// Why: an interrupt DISCARDS the open construct. A closing quote or a bare
/// Enter would instead RUN whatever had accumulated — on this issue's live
/// pane that was three concatenated launch commands inside one open quote.
/// What: pane-scoped when known (sibling-window hijack, #2456). Best-effort:
/// the trait default for both methods is an error, so a driver with no
/// interrupt support simply skips it and the next probe reports the result.
/// Wait `interval` without parking a Tokio worker (#8233 round 3, finding 5).
///
/// Why: every `RuntimeAdapter` entry point is synchronous, but four async call
/// sites (`lifecycle::resume_managed`, `launch_on_main`, `auto_relaunch`) reach
/// this from the runtime that also serves the daemon's HTTP routes. A bare
/// `std::thread::sleep` of up to 3 s there stalls whatever else that worker was
/// going to run — the discipline `launch_verify::verify_launch` documents.
/// What: on a multi-thread runtime, `block_in_place` hands this task's worker
/// back to the scheduler for the duration; anywhere else (a current-thread
/// runtime, or no runtime at all — the CLI's own launch path) it is a plain
/// sleep, because `block_in_place` panics outside a multi-thread runtime.
/// `spawn_blocking` was the review's suggestion and is rejected here: it needs
/// every argument `Send + 'static`, so it would clone the driver `Arc` and the
/// spec at four call sites to move a wait that `block_in_place` moves in place —
/// and the #8233 claim `resume_managed` holds is a local guard in that future,
/// which `block_in_place` leaves exactly where it is.
/// Test: `the_handshake_does_not_park_a_tokio_worker`.
/// Prove the pane's shell is executing what it is typed, recovering once.
///
/// Why: see this module's header — this is the CONFIRMED prompt that finding 1
/// says the launcher was typing without.
/// What: captures the tail once to decide observability (a driver that cannot
/// capture answers [`PaneState::Unobservable`] immediately and pays no delay).
/// Then, for up to `rounds`: interrupt the pane when a continuation prompt is
/// visible or a previous round failed, type [`probe_line`], and poll the tail
/// `attempts` times for [`probe_output`]. First sighting is
/// [`PaneState::Ready`]. Exhaustion reports [`PaneState::Continuation`] when a
/// continuation prompt was seen in any round, else [`PaneState::Unresponsive`].
/// A probe the driver refuses to send is [`PaneState::Unresponsive`] at once —
/// a pane that cannot be typed into cannot be launched into either.
///
/// Waits through [`park`], which never stalls a Tokio worker;
/// `attempts`/`interval` are parameters so tests spend none of the production
/// budget.
/// Test: `confirm_prompt_is_ready_when_the_probe_echoes`,
/// `an_unobservable_session_is_not_called_unresponsive`,
/// `the_handshake_does_not_park_a_tokio_worker`,
/// `confirm_prompt_reports_a_wedged_continuation_prompt`,
/// `confirm_prompt_reports_an_unresponsive_shell`,
/// `confirm_prompt_interrupts_a_continuation_prompt_before_probing`,
/// `confirm_prompt_recovers_a_pane_that_frees_up_after_the_interrupt`,
/// `confirm_prompt_is_unobservable_when_the_driver_cannot_capture`,
/// `confirm_prompt_is_unobservable_when_the_existence_probe_errors`,
/// `confirm_prompt_never_sends_a_closing_quote_or_a_bare_enter`.
pub