par_term/tab/session_logging.rs
1//! Tab session logging methods.
2//!
3//! Provides methods for toggling session logging on/off and querying its state.
4
5use crate::config::{Config, SessionLogFormat};
6use crate::session_logger::SessionLogger;
7use crate::tab::Tab;
8use std::sync::Arc;
9
10impl Tab {
11 /// Toggle session logging on/off.
12 ///
13 /// Returns `Ok(true)` if logging is now active, `Ok(false)` if stopped.
14 /// If logging wasn't active and no logger exists, creates a new one.
15 pub fn toggle_session_logging(&mut self, config: &Config) -> anyhow::Result<bool> {
16 let mut logger_guard = self.session_logger.lock();
17
18 if let Some(ref mut logger) = *logger_guard {
19 // Logger exists - toggle based on current state
20 if logger.is_active() {
21 logger.stop()?;
22 log::info!("Session logging stopped via hotkey");
23 Ok(false)
24 } else {
25 logger.start()?;
26 log::info!("Session logging started via hotkey");
27 Ok(true)
28 }
29 } else {
30 // No logger exists - create one and start it
31 let logs_dir = config.logs_dir();
32 if let Err(e) = std::fs::create_dir_all(&logs_dir) {
33 log::warn!("Failed to create logs directory: {}", e);
34 return Err(anyhow::anyhow!("Failed to create logs directory: {}", e));
35 }
36 // SEC-010: Enforce restrictive directory permissions so that session logs
37 // (which may capture terminal output including passwords) are not
38 // world-listable even if the user's umask is permissive.
39 #[cfg(unix)]
40 {
41 use std::os::unix::fs::PermissionsExt;
42 let _ = std::fs::set_permissions(&logs_dir, std::fs::Permissions::from_mode(0o700));
43 }
44
45 // Get terminal dimensions
46 let dimensions = if let Ok(term) = self.terminal.try_read() {
47 term.dimensions()
48 } else {
49 (80, 24) // fallback
50 };
51
52 let session_title = Some(format!(
53 "{} - {}",
54 self.title,
55 chrono::Local::now().format("%Y-%m-%d %H:%M:%S")
56 ));
57
58 let mut logger = SessionLogger::new(
59 config.session_log.session_log_format,
60 &logs_dir,
61 dimensions,
62 session_title,
63 )?;
64
65 logger.set_redact_passwords(config.session_log.session_log_redact_passwords);
66
67 // v3 logs get the graphics-aware exporter: it ferries the recording
68 // to the core lib's export through the terminal at stop() time.
69 if config.session_log.session_log_format == SessionLogFormat::AsciicastV3 {
70 let tm = Arc::clone(&self.terminal);
71 logger.set_v3_exporter(Box::new(move |session| {
72 tm.try_read()
73 .ok()
74 .map(|tm| tm.export_session_asciicast_v3(session))
75 }));
76 }
77
78 logger.start()?;
79
80 // SEC-002: Emit a prominent one-time warning when session logging is enabled.
81 // Session logs capture raw terminal I/O and may include passwords, API keys,
82 // tokens, and other credentials even with heuristic redaction active.
83 eprintln!(
84 "\n[par-term SESSION LOGGING ENABLED]\n\
85 Session output is now being recorded to: {}\n\
86 WARNING: Session logs may capture sensitive data (passwords, API keys, tokens)\n\
87 despite heuristic redaction. Password-prompt detection is enabled: {}\n\
88 Do NOT share session log files unless you have reviewed their contents.\n\
89 Disable session logging when working with sensitive credentials.\n",
90 logs_dir.display(),
91 config.session_log.session_log_redact_passwords,
92 );
93
94 // Set up output callback to record PTY output
95 let logger_clone = Arc::clone(&self.session_logger);
96 if let Ok(term) = self.terminal.try_read() {
97 term.set_output_callback(move |data: &[u8]| {
98 if let Some(ref mut logger) = *logger_clone.lock() {
99 logger.record_output(data);
100 }
101 });
102 }
103
104 *logger_guard = Some(logger);
105 drop(logger_guard);
106
107 // In a mux tab the panes are mirrors fed through
108 // `process_mux_output`, which fires the per-terminal output
109 // callback — they need the same attachment or their output
110 // never reaches the log.
111 self.attach_session_logging_to_panes();
112 log::info!("Session logging created and started via hotkey");
113 Ok(true)
114 }
115 }
116
117 /// Check if session logging is currently active.
118 pub fn is_session_logging_active(&self) -> bool {
119 if let Some(ref logger) = *self.session_logger.lock() {
120 logger.is_active()
121 } else {
122 false
123 }
124 }
125
126 /// Attach the tab's session-logger output callback to every pane
127 /// terminal of a tmux/mux tab.
128 ///
129 /// Mirror panes receive daemon output through `process_mux_output`,
130 /// which fires the per-terminal output callback; the toggle/auto-log
131 /// paths attach only on `tab.terminal`, which in a mux tab is the
132 /// hidden shell whose output is not what the user sees. Called when
133 /// logging starts and whenever a tmux layout change creates panes, so
134 /// mirrors born mid-session log too. The callback captures the shared
135 /// logger slot, so one attached before a later toggle still picks up
136 /// the logger stored into it.
137 pub(crate) fn attach_session_logging_to_panes(&self) {
138 if self.session_logger.lock().is_none() {
139 return;
140 }
141 if !(self.tmux.tmux_gateway_active || self.tmux.tmux_pane_id.is_some()) {
142 return;
143 }
144 if let Some(pm) = self.pane_manager() {
145 for pane in pm.all_panes() {
146 let logger_clone = Arc::clone(&self.session_logger);
147 if let Ok(term) = pane.terminal.try_read() {
148 term.set_output_callback(move |data: &[u8]| {
149 if let Some(ref mut logger) = *logger_clone.lock() {
150 logger.record_output(data);
151 }
152 });
153 }
154 }
155 }
156 }
157}