Skip to main content

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}