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
//! Tab session logging methods.
//!
//! Provides methods for toggling session logging on/off and querying its state.
use crate::config::{Config, SessionLogFormat};
use crate::session_logger::SessionLogger;
use crate::tab::Tab;
use std::sync::Arc;
impl Tab {
/// Toggle session logging on/off.
///
/// Returns `Ok(true)` if logging is now active, `Ok(false)` if stopped.
/// If logging wasn't active and no logger exists, creates a new one.
pub fn toggle_session_logging(&mut self, config: &Config) -> anyhow::Result<bool> {
let mut logger_guard = self.session_logger.lock();
if let Some(ref mut logger) = *logger_guard {
// Logger exists - toggle based on current state
if logger.is_active() {
logger.stop()?;
log::info!("Session logging stopped via hotkey");
Ok(false)
} else {
logger.start()?;
log::info!("Session logging started via hotkey");
Ok(true)
}
} else {
// No logger exists - create one and start it
let logs_dir = config.logs_dir();
if let Err(e) = std::fs::create_dir_all(&logs_dir) {
log::warn!("Failed to create logs directory: {}", e);
return Err(anyhow::anyhow!("Failed to create logs directory: {}", e));
}
// SEC-010: Enforce restrictive directory permissions so that session logs
// (which may capture terminal output including passwords) are not
// world-listable even if the user's umask is permissive.
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(&logs_dir, std::fs::Permissions::from_mode(0o700));
}
// Get terminal dimensions
let dimensions = if let Ok(term) = self.terminal.try_read() {
term.dimensions()
} else {
(80, 24) // fallback
};
let session_title = Some(format!(
"{} - {}",
self.title,
chrono::Local::now().format("%Y-%m-%d %H:%M:%S")
));
let mut logger = SessionLogger::new(
config.session_log.session_log_format,
&logs_dir,
dimensions,
session_title,
)?;
logger.set_redact_passwords(config.session_log.session_log_redact_passwords);
// v3 logs get the graphics-aware exporter: it ferries the recording
// to the core lib's export through the terminal at stop() time.
if config.session_log.session_log_format == SessionLogFormat::AsciicastV3 {
let tm = Arc::clone(&self.terminal);
logger.set_v3_exporter(Box::new(move |session| {
tm.try_read()
.ok()
.map(|tm| tm.export_session_asciicast_v3(session))
}));
}
logger.start()?;
// SEC-002: Emit a prominent one-time warning when session logging is enabled.
// Session logs capture raw terminal I/O and may include passwords, API keys,
// tokens, and other credentials even with heuristic redaction active.
eprintln!(
"\n[par-term SESSION LOGGING ENABLED]\n\
Session output is now being recorded to: {}\n\
WARNING: Session logs may capture sensitive data (passwords, API keys, tokens)\n\
despite heuristic redaction. Password-prompt detection is enabled: {}\n\
Do NOT share session log files unless you have reviewed their contents.\n\
Disable session logging when working with sensitive credentials.\n",
logs_dir.display(),
config.session_log.session_log_redact_passwords,
);
// Set up output callback to record PTY output
let logger_clone = Arc::clone(&self.session_logger);
if let Ok(term) = self.terminal.try_read() {
term.set_output_callback(move |data: &[u8]| {
if let Some(ref mut logger) = *logger_clone.lock() {
logger.record_output(data);
}
});
}
*logger_guard = Some(logger);
drop(logger_guard);
// In a mux tab the panes are mirrors fed through
// `process_mux_output`, which fires the per-terminal output
// callback — they need the same attachment or their output
// never reaches the log.
self.attach_session_logging_to_panes();
log::info!("Session logging created and started via hotkey");
Ok(true)
}
}
/// Check if session logging is currently active.
pub fn is_session_logging_active(&self) -> bool {
if let Some(ref logger) = *self.session_logger.lock() {
logger.is_active()
} else {
false
}
}
/// Attach the tab's session-logger output callback to every pane
/// terminal of a tmux/mux tab.
///
/// Mirror panes receive daemon output through `process_mux_output`,
/// which fires the per-terminal output callback; the toggle/auto-log
/// paths attach only on `tab.terminal`, which in a mux tab is the
/// hidden shell whose output is not what the user sees. Called when
/// logging starts and whenever a tmux layout change creates panes, so
/// mirrors born mid-session log too. The callback captures the shared
/// logger slot, so one attached before a later toggle still picks up
/// the logger stored into it.
pub(crate) fn attach_session_logging_to_panes(&self) {
if self.session_logger.lock().is_none() {
return;
}
if !(self.tmux.tmux_gateway_active || self.tmux.tmux_pane_id.is_some()) {
return;
}
if let Some(pm) = self.pane_manager() {
for pane in pm.all_panes() {
let logger_clone = Arc::clone(&self.session_logger);
if let Ok(term) = pane.terminal.try_read() {
term.set_output_callback(move |data: &[u8]| {
if let Some(ref mut logger) = *logger_clone.lock() {
logger.record_output(data);
}
});
}
}
}
}
}