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
//! Whether a pane operation may act on a record's pane (#9101).
//!
//! Why: send, inject, answer, observe, capture and resume address a record's
//! tmux pane by its `%N` id. A restarted tmux server hands the same ids out
//! again, so a stale record's `%N` can name a pane in an unrelated live
//! session, and those operations then typed into it or read it. #9004 proved
//! ownership for the teardown paths only.
//! What: [`SessionManager::owned_pane`], the one gate every pane operation
//! runs before it touches tmux. It reuses the #9004 `same_server` verdict.
//! Test: `pane_ops_identity_tests.rs`; the pane-gone refusal in
//! `pane_scoped_tests.rs`.
use tracing::warn;
use super::manager::{ManagedError, SessionManager};
use super::record::{ManagedSessionId, SessionRecord};
use super::runtime_identity::{RuntimeOwnership, same_server};
impl SessionManager {
/// The record's own pane id, proven to be on the live tmux server, or the
/// refusal to return before any pane operation (#9101).
///
/// Why: see the module doc. A pane id alone is not proof of ownership.
/// What: `PaneGone` when the session's pane list lacks `record.pane_id`.
/// `InvalidState` when the record has no pane id, the pane list cannot be
/// read, or `same_server` is not `Owned`: no server identity, an
/// unreadable identity, another server, or another session. `Ok` carries
/// the pane id only when the verdict is `Owned`.
/// Test: `a_stale_record_after_a_server_restart_never_sends_keys`,
/// `an_unreadable_pane_identity_refuses_every_pane_operation`,
/// `a_record_without_a_pane_id_refuses_every_pane_operation`.
pub(crate) fn owned_pane(
&self,
id: &ManagedSessionId,
record: &SessionRecord,
) -> Result<String, ManagedError> {
let name = record.tmux_name.as_str();
let Some(pane_id) = record.pane_id.as_deref() else {
return Err(refusal(
id,
record,
format!(
"this record carries no pane id to prove tmux session '{name}' is its own; {}",
recovery(id, name)
),
));
};
match self.tmux.pane_exists_checked(name, pane_id) {
Some(true) => {}
Some(false) => {
warn!(
id = %id, name = %name, pane_id = %pane_id,
"recorded pane is gone but the tmux session is still alive (sibling window) \
— refusing to operate on an unrelated active pane"
);
return Err(ManagedError::PaneGone(id.to_string(), pane_id.to_owned()));
}
None => {
return Err(refusal(
id,
record,
format!(
"the panes of tmux session '{name}' could not be listed to find {pane_id}"
),
));
}
}
match same_server(record, name, pane_id, self.tmux.as_ref()) {
RuntimeOwnership::Owned { pane_id, .. } => Ok(pane_id),
RuntimeOwnership::Foreign(why) => Err(refusal(
id,
record,
format!("{why}; the record is stale, so delete it"),
)),
RuntimeOwnership::Unverifiable(why) => Err(refusal(
id,
record,
format!("{why}; {}", recovery(id, name)),
)),
// `same_server` never answers `Absent`; refuse rather than act.
RuntimeOwnership::Absent => Err(refusal(
id,
record,
format!("no tmux session '{name}' holds pane {pane_id}"),
)),
}
}
/// The process in the foreground of pane `pane_id`, from the manager's own
/// tmux driver (#9566).
/// Test: `a_stopped_record_whose_pane_runs_a_live_agent_is_never_reactivated_in_place`.
pub(crate) fn pane_foreground_command(&self, pane_id: &str) -> Result<String, ManagedError> {
self.tmux.pane_foreground_command(pane_id)
}
}
/// The recovery an operator runs for a record whose session the gate cannot
/// prove (#9101): end the session, then let resume recreate it, which
/// captures the new pane with its server.
fn recovery(id: &ManagedSessionId, name: &str) -> String {
format!(
"if that session is this record's, end it with `tmux kill-session -t {}`, \
then run `tm session resume {id}` to recreate it",
trusty_common::tmux::shell_exact_session_target(name)
)
}
/// The typed refusal for an unproven pane, logged at `warn` (#9101).
fn refusal(id: &ManagedSessionId, record: &SessionRecord, why: String) -> ManagedError {
warn!(id = %id, name = %record.tmux_name, "{why}; no pane operation ran (#9101)");
ManagedError::InvalidState(
id.to_string(),
format!("refusing to act on the recorded pane: {why}"),
)
}