1use parking_lot::Mutex;
7use portable_pty::{MasterPty, PtySize as PortablePtySize, native_pty_system};
8use serde::{Deserialize, Serialize};
9use std::sync::Arc;
10use std::time::Instant;
11use tokio::sync::mpsc;
12
13use crate::config::PtyConfig;
14
15use super::error::PtyError;
16
17pub type PtySessionId = String;
19
20#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
22pub struct PtySize {
23 pub cols: u16,
24 pub rows: u16,
25 pub pixel_width: u16,
26 pub pixel_height: u16,
27}
28
29impl PtySize {
30 pub fn to_portable(self) -> PortablePtySize {
32 PortablePtySize {
33 rows: self.rows,
34 cols: self.cols,
35 pixel_width: self.pixel_width,
36 pixel_height: self.pixel_height,
37 }
38 }
39
40 pub fn default_80x24() -> Self {
42 Self {
43 cols: 80,
44 rows: 24,
45 pixel_width: 0,
46 pixel_height: 0,
47 }
48 }
49}
50
51pub enum PtySessionState {
53 Attached {
55 ws_tx: mpsc::Sender<Vec<u8>>,
57 },
58 Detached {
60 orphan_since: Instant,
62 },
63 Closed {
65 exit_code: Option<i32>,
67 signal: Option<i32>,
69 at: Instant,
71 },
72}
73
74#[derive(Debug, Clone, Serialize)]
76pub struct PtySessionInfo {
77 pub id: PtySessionId,
78 pub shell: String,
79 pub created_at_unix_ms: u64,
80 pub last_input_at_unix_ms: u64,
81 pub state: String,
82 pub cols: u16,
83 pub rows: u16,
84}
85
86pub struct PtySession {
88 pub id: PtySessionId,
89 pub principal: String,
90 pub shell: String,
91 pub created_at: Instant,
92 pub size: PtySize,
93 pub last_input_ms: parking_lot::Mutex<u64>,
95 pub master: Mutex<Option<Box<dyn MasterPty + Send>>>,
97 pub state: Mutex<PtySessionState>,
98}
99
100impl PtySession {
101 pub fn spawn(
103 id: PtySessionId,
104 principal: String,
105 shell: String,
106 size: PtySize,
107 config: &PtyConfig,
108 ) -> Result<Arc<Self>, PtyError> {
109 let pty_system = native_pty_system();
110 let pair = pty_system
111 .openpty(PortablePtySize {
112 rows: size.rows,
113 cols: size.cols,
114 pixel_width: size.pixel_width,
115 pixel_height: size.pixel_height,
116 })
117 .map_err(|e| PtyError::Spawn(e.to_string()))?;
118
119 let mut cmd = portable_pty::CommandBuilder::new(&shell);
120 if let Some(cwd) = &config.working_directory {
121 cmd.cwd(cwd);
122 }
123 cmd.env("TERM", "xterm-256color");
124 cmd.env("COLORTERM", "truecolor");
125 cmd.env("OXIOS_PTY_SESSION", &id);
126 for (k, v) in std::env::vars() {
128 if config
129 .env_strip_prefixes
130 .iter()
131 .any(|p| k.starts_with(p.as_str()))
132 {
133 continue;
134 }
135 cmd.env(&k, &v);
136 }
137 for (k, v) in &config.extra_env {
138 cmd.env(k, v);
139 }
140
141 let mut child = pair
142 .slave
143 .spawn_command(cmd)
144 .map_err(|e| PtyError::Spawn(e.to_string()))?;
145 drop(pair.slave);
146
147 let now_unix_ms = unix_millis_now();
148 let session = Arc::new(Self {
149 id,
150 principal,
151 shell,
152 created_at: Instant::now(),
153 size,
154 last_input_ms: parking_lot::Mutex::new(now_unix_ms),
155 master: Mutex::new(Some(pair.master)),
156 state: Mutex::new(PtySessionState::Detached {
157 orphan_since: Instant::now(),
158 }),
159 });
160
161 let session_for_wait = Arc::clone(&session);
163 tokio::spawn(async move {
164 let exit = tokio::task::spawn_blocking(move || child.wait()).await;
165 let (code, signal) = match exit {
166 Ok(Ok(status)) => (Some(status.exit_code() as i32), None),
167 Ok(Err(e)) => {
168 tracing::warn!(error = %e, session = %session_for_wait.id, "pty child wait error");
169 (None, None)
170 }
171 Err(e) => {
172 tracing::warn!(error = %e, session = %session_for_wait.id, "pty wait join error");
173 (None, None)
174 }
175 };
176 let mut st = session_for_wait.state.lock();
177 *st = PtySessionState::Closed {
178 exit_code: code,
179 signal,
180 at: Instant::now(),
181 };
182 });
183
184 Ok(session)
185 }
186
187 pub fn touch_input(&self) {
189 let mut g = self.last_input_ms.lock();
190 *g = unix_millis_now();
191 }
192
193 pub fn idle_ms(&self) -> u64 {
195 unix_millis_now().saturating_sub(*self.last_input_ms.lock())
196 }
197
198 pub fn lifetime_ms(&self) -> u64 {
200 self.created_at.elapsed().as_millis() as u64
201 }
202
203 pub fn info(&self) -> PtySessionInfo {
205 let state_label = match &*self.state.lock() {
206 PtySessionState::Attached { .. } => "attached",
207 PtySessionState::Detached { .. } => "detached",
208 PtySessionState::Closed { .. } => "closed",
209 };
210 PtySessionInfo {
211 id: self.id.clone(),
212 shell: self.shell.clone(),
213 created_at_unix_ms: unix_millis_from(self.created_at),
214 last_input_at_unix_ms: *self.last_input_ms.lock(),
215 state: state_label.to_string(),
216 cols: self.size.cols,
217 rows: self.size.rows,
218 }
219 }
220}
221
222impl Drop for PtySession {
224 fn drop(&mut self) {
225 let _ = self.master.lock().take();
226 }
227}
228
229fn unix_millis_now() -> u64 {
230 use std::time::{SystemTime, UNIX_EPOCH};
231 SystemTime::now()
232 .duration_since(UNIX_EPOCH)
233 .map(|d| d.as_millis() as u64)
234 .unwrap_or(0)
235}
236
237fn unix_millis_from(t: Instant) -> u64 {
238 let now = Instant::now();
239 let delta = now.saturating_duration_since(t);
240 unix_millis_now().saturating_sub(delta.as_millis() as u64)
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn size_default_80x24() {
249 let s = PtySize::default_80x24();
250 assert_eq!(s.cols, 80);
251 assert_eq!(s.rows, 24);
252 }
253
254 #[test]
255 fn size_to_portable() {
256 let s = PtySize {
257 cols: 120,
258 rows: 40,
259 pixel_width: 0,
260 pixel_height: 0,
261 };
262 let p = s.to_portable();
263 assert_eq!(p.cols, 120);
264 assert_eq!(p.rows, 40);
265 }
266}